@hef2024/llmasaservice-ui 0.21.0 → 0.22.1
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/AICHATPANEL-PORT-INVENTORY.md +2 -0
- package/CONTROLLED-COLLAPSE-IMPLEMENTATION.md +274 -0
- package/DEBUG-ERROR-HANDLING.md +2 -0
- package/FIX-APPLIED.md +2 -0
- package/IMPLEMENTATION-COMPLETE.md +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +53 -11
- package/dist/index.mjs +53 -11
- package/docs/CHANGELOG-ERROR-HANDLING.md +2 -0
- package/docs/CONTROLLED-COLLAPSE-QUICK-START.md +147 -0
- package/docs/CONTROLLED-COLLAPSE-STATE.md +651 -0
- package/docs/CONVERSATION-HISTORY.md +2 -0
- package/docs/ERROR-HANDLING-413.md +2 -0
- package/docs/ERROR-HANDLING-SUMMARY.md +2 -0
- package/package.json +1 -1
- package/src/AIAgentPanel.tsx +59 -6
- package/src/AIChatPanel.tsx +25 -5
- package/src/components/ui/Button.tsx +2 -0
- package/src/components/ui/Dialog.tsx +2 -0
- package/src/components/ui/Input.tsx +2 -0
- package/src/components/ui/Select.tsx +2 -0
- package/src/components/ui/ToolInfoModal.tsx +2 -0
- package/src/components/ui/Tooltip.tsx +2 -0
- package/src/components/ui/index.ts +2 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
# Controlled Collapse State for AIAgentPanel
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `AIAgentPanel` component now supports both **controlled** and **uncontrolled** collapse state patterns, following standard React conventions. This allows parent components to manage and persist the panel's collapsed/expanded state across page navigations and user sessions.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- ✅ **Backward compatible** - Existing code continues to work without changes
|
|
10
|
+
- ✅ **Standard React pattern** - Follows established controlled/uncontrolled conventions
|
|
11
|
+
- ✅ **Flexible persistence** - Supports localStorage, database, Redux, etc.
|
|
12
|
+
- ✅ **Better UX** - Users don't lose their panel preference when navigating
|
|
13
|
+
- ✅ **Opt-in** - Developers can choose controlled mode only when needed
|
|
14
|
+
- ✅ **Dev mode warnings** - Helpful warnings for common mistakes
|
|
15
|
+
|
|
16
|
+
## API Reference
|
|
17
|
+
|
|
18
|
+
### New Props
|
|
19
|
+
|
|
20
|
+
| Prop | Type | Default | Description |
|
|
21
|
+
|------|------|---------|-------------|
|
|
22
|
+
| `isCollapsed` | `boolean \| undefined` | `undefined` | Controlled collapse state. When provided, makes the component controlled. |
|
|
23
|
+
| `onCollapsedChange` | `(isCollapsed: boolean) => void \| undefined` | `undefined` | Callback fired when collapse state changes. Works in both controlled and uncontrolled modes. |
|
|
24
|
+
| `defaultCollapsed` | `boolean` | `false` | Initial collapse state for uncontrolled mode. Ignored when `isCollapsed` is provided. |
|
|
25
|
+
| `collapsible` | `boolean` | `true` | Whether the panel can be collapsed at all. |
|
|
26
|
+
|
|
27
|
+
### Behavior
|
|
28
|
+
|
|
29
|
+
#### Uncontrolled Mode (Default)
|
|
30
|
+
- `isCollapsed` is `undefined`
|
|
31
|
+
- Component manages its own internal collapse state
|
|
32
|
+
- `defaultCollapsed` sets the initial state
|
|
33
|
+
- `onCollapsedChange` fires when state changes (optional)
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Uncontrolled - panel manages its own state
|
|
37
|
+
<AIAgentPanel
|
|
38
|
+
defaultCollapsed={false}
|
|
39
|
+
agents={agents}
|
|
40
|
+
customerId={customerId}
|
|
41
|
+
apiKey={apiKey}
|
|
42
|
+
/>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### Controlled Mode
|
|
46
|
+
- `isCollapsed` is provided
|
|
47
|
+
- Component uses prop value instead of internal state
|
|
48
|
+
- Parent component is responsible for managing state
|
|
49
|
+
- `onCollapsedChange` should be provided to allow user interaction
|
|
50
|
+
- `defaultCollapsed` is ignored (warning in dev mode)
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// Controlled - parent manages state
|
|
54
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
55
|
+
|
|
56
|
+
<AIAgentPanel
|
|
57
|
+
isCollapsed={collapsed}
|
|
58
|
+
onCollapsedChange={setCollapsed}
|
|
59
|
+
agents={agents}
|
|
60
|
+
customerId={customerId}
|
|
61
|
+
apiKey={apiKey}
|
|
62
|
+
/>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage Examples
|
|
66
|
+
|
|
67
|
+
### 1. Uncontrolled with Callback (Analytics)
|
|
68
|
+
|
|
69
|
+
Track collapse events without controlling the state:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<AIAgentPanel
|
|
73
|
+
defaultCollapsed={false}
|
|
74
|
+
onCollapsedChange={(isCollapsed) => {
|
|
75
|
+
console.log('Panel collapsed:', isCollapsed);
|
|
76
|
+
analytics.track('panel_collapsed', { isCollapsed });
|
|
77
|
+
}}
|
|
78
|
+
agents={agents}
|
|
79
|
+
customerId={customerId}
|
|
80
|
+
apiKey={apiKey}
|
|
81
|
+
/>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2. Controlled with localStorage Persistence
|
|
85
|
+
|
|
86
|
+
Persist the collapse preference across page navigations:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
const [collapsed, setCollapsed] = useState(() => {
|
|
90
|
+
if (typeof window !== 'undefined') {
|
|
91
|
+
return localStorage.getItem('aiPanelCollapsed') === 'true';
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const handleCollapsedChange = (isCollapsed: boolean) => {
|
|
97
|
+
setCollapsed(isCollapsed);
|
|
98
|
+
localStorage.setItem('aiPanelCollapsed', String(isCollapsed));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<AIAgentPanel
|
|
103
|
+
isCollapsed={collapsed}
|
|
104
|
+
onCollapsedChange={handleCollapsedChange}
|
|
105
|
+
agents={agents}
|
|
106
|
+
customerId={customerId}
|
|
107
|
+
apiKey={apiKey}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 3. Controlled with React Query/Database
|
|
113
|
+
|
|
114
|
+
Sync collapse state across devices and sessions:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
const { data: userPreferences, mutate } = useUserPreferences();
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<AIAgentPanel
|
|
121
|
+
isCollapsed={userPreferences?.aiPanelCollapsed ?? false}
|
|
122
|
+
onCollapsedChange={(isCollapsed) => {
|
|
123
|
+
mutate({ aiPanelCollapsed: isCollapsed });
|
|
124
|
+
}}
|
|
125
|
+
agents={agents}
|
|
126
|
+
customerId={customerId}
|
|
127
|
+
apiKey={apiKey}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 4. Multi-Page Application (Main Use Case)
|
|
133
|
+
|
|
134
|
+
Panel state persists as users navigate between pages:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
function App() {
|
|
138
|
+
const [currentPage, setCurrentPage] = useState('dashboard');
|
|
139
|
+
const [panelCollapsed, setPanelCollapsed] = useState(() => {
|
|
140
|
+
return localStorage.getItem('aiPanelCollapsed') === 'true';
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const handleCollapsedChange = (isCollapsed: boolean) => {
|
|
144
|
+
setPanelCollapsed(isCollapsed);
|
|
145
|
+
localStorage.setItem('aiPanelCollapsed', String(isCollapsed));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div>
|
|
150
|
+
<nav>
|
|
151
|
+
<button onClick={() => setCurrentPage('dashboard')}>Dashboard</button>
|
|
152
|
+
<button onClick={() => setCurrentPage('profile')}>Profile</button>
|
|
153
|
+
<button onClick={() => setCurrentPage('settings')}>Settings</button>
|
|
154
|
+
</nav>
|
|
155
|
+
|
|
156
|
+
<main>
|
|
157
|
+
{/* Page content changes, but panel state persists */}
|
|
158
|
+
{currentPage === 'dashboard' && <Dashboard />}
|
|
159
|
+
{currentPage === 'profile' && <Profile />}
|
|
160
|
+
{currentPage === 'settings' && <Settings />}
|
|
161
|
+
</main>
|
|
162
|
+
|
|
163
|
+
{/* Panel remains collapsed/expanded across all pages */}
|
|
164
|
+
<AIAgentPanel
|
|
165
|
+
isCollapsed={panelCollapsed}
|
|
166
|
+
onCollapsedChange={handleCollapsedChange}
|
|
167
|
+
agents={agents}
|
|
168
|
+
customerId={customerId}
|
|
169
|
+
apiKey={apiKey}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 5. Programmatic Control
|
|
177
|
+
|
|
178
|
+
Control the panel from other UI elements:
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div>
|
|
185
|
+
<button onClick={() => setCollapsed(false)}>Expand Panel</button>
|
|
186
|
+
<button onClick={() => setCollapsed(true)}>Collapse Panel</button>
|
|
187
|
+
|
|
188
|
+
<AIAgentPanel
|
|
189
|
+
isCollapsed={collapsed}
|
|
190
|
+
onCollapsedChange={setCollapsed}
|
|
191
|
+
agents={agents}
|
|
192
|
+
customerId={customerId}
|
|
193
|
+
apiKey={apiKey}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 6. Responsive Behavior
|
|
200
|
+
|
|
201
|
+
Auto-collapse on mobile devices:
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
const [collapsed, setCollapsed] = useState(() => {
|
|
205
|
+
if (typeof window !== 'undefined') {
|
|
206
|
+
return window.innerWidth < 768;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
const handleResize = () => {
|
|
213
|
+
if (window.innerWidth < 768 && !collapsed) {
|
|
214
|
+
setCollapsed(true);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
window.addEventListener('resize', handleResize);
|
|
219
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
220
|
+
}, [collapsed]);
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<AIAgentPanel
|
|
224
|
+
isCollapsed={collapsed}
|
|
225
|
+
onCollapsedChange={setCollapsed}
|
|
226
|
+
agents={agents}
|
|
227
|
+
customerId={customerId}
|
|
228
|
+
apiKey={apiKey}
|
|
229
|
+
/>
|
|
230
|
+
);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Dev Mode Warnings
|
|
234
|
+
|
|
235
|
+
The component provides helpful warnings in development mode:
|
|
236
|
+
|
|
237
|
+
### Warning 1: Controlled without Callback
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
// ⚠️ Warning: isCollapsed provided without onCollapsedChange
|
|
241
|
+
<AIAgentPanel
|
|
242
|
+
isCollapsed={true}
|
|
243
|
+
// Missing onCollapsedChange - user can't interact!
|
|
244
|
+
/>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Warning message:**
|
|
248
|
+
```
|
|
249
|
+
AIAgentPanel: You provided `isCollapsed` prop without `onCollapsedChange`.
|
|
250
|
+
This will render a read-only collapsed state. To allow user interaction, provide both props.
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Warning 2: Conflicting Props
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
// ⚠️ Warning: Both isCollapsed and defaultCollapsed provided
|
|
257
|
+
<AIAgentPanel
|
|
258
|
+
isCollapsed={collapsed}
|
|
259
|
+
onCollapsedChange={setCollapsed}
|
|
260
|
+
defaultCollapsed={true} // This will be ignored!
|
|
261
|
+
/>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Warning message:**
|
|
265
|
+
```
|
|
266
|
+
AIAgentPanel: You provided both `isCollapsed` and `defaultCollapsed` props.
|
|
267
|
+
When using controlled mode (isCollapsed), the defaultCollapsed prop is ignored.
|
|
268
|
+
Remove defaultCollapsed to avoid confusion.
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Migration Guide
|
|
272
|
+
|
|
273
|
+
### From Uncontrolled to Controlled
|
|
274
|
+
|
|
275
|
+
**Before (Uncontrolled):**
|
|
276
|
+
```tsx
|
|
277
|
+
<AIAgentPanel
|
|
278
|
+
defaultCollapsed={false}
|
|
279
|
+
agents={agents}
|
|
280
|
+
customerId={customerId}
|
|
281
|
+
apiKey={apiKey}
|
|
282
|
+
/>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**After (Controlled with localStorage):**
|
|
286
|
+
```tsx
|
|
287
|
+
const [collapsed, setCollapsed] = useState(() => {
|
|
288
|
+
return localStorage.getItem('aiPanelCollapsed') === 'true';
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const handleCollapsedChange = (isCollapsed: boolean) => {
|
|
292
|
+
setCollapsed(isCollapsed);
|
|
293
|
+
localStorage.setItem('aiPanelCollapsed', String(isCollapsed));
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
<AIAgentPanel
|
|
297
|
+
isCollapsed={collapsed}
|
|
298
|
+
onCollapsedChange={handleCollapsedChange}
|
|
299
|
+
agents={agents}
|
|
300
|
+
customerId={customerId}
|
|
301
|
+
apiKey={apiKey}
|
|
302
|
+
/>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### No Migration Required
|
|
306
|
+
|
|
307
|
+
If you're happy with the current behavior, you don't need to change anything. The component remains fully backward compatible.
|
|
308
|
+
|
|
309
|
+
## Persistence Strategies
|
|
310
|
+
|
|
311
|
+
### 1. localStorage (Client-side)
|
|
312
|
+
|
|
313
|
+
**Pros:**
|
|
314
|
+
- Simple to implement
|
|
315
|
+
- No backend required
|
|
316
|
+
- Fast access
|
|
317
|
+
|
|
318
|
+
**Cons:**
|
|
319
|
+
- Not synced across devices
|
|
320
|
+
- Cleared when user clears browser data
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
const [collapsed, setCollapsed] = useState(() => {
|
|
324
|
+
return localStorage.getItem('aiPanelCollapsed') === 'true';
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const handleCollapsedChange = (isCollapsed: boolean) => {
|
|
328
|
+
setCollapsed(isCollapsed);
|
|
329
|
+
localStorage.setItem('aiPanelCollapsed', String(isCollapsed));
|
|
330
|
+
};
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### 2. Database (Server-side)
|
|
334
|
+
|
|
335
|
+
**Pros:**
|
|
336
|
+
- Synced across devices
|
|
337
|
+
- Persistent across browser clears
|
|
338
|
+
- Can be part of user preferences
|
|
339
|
+
|
|
340
|
+
**Cons:**
|
|
341
|
+
- Requires backend API
|
|
342
|
+
- Network latency
|
|
343
|
+
- More complex implementation
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
const { data: preferences } = useUserPreferences();
|
|
347
|
+
const { mutate: updatePreferences } = useUpdateUserPreferences();
|
|
348
|
+
|
|
349
|
+
<AIAgentPanel
|
|
350
|
+
isCollapsed={preferences?.aiPanelCollapsed ?? false}
|
|
351
|
+
onCollapsedChange={(isCollapsed) => {
|
|
352
|
+
updatePreferences({ aiPanelCollapsed: isCollapsed });
|
|
353
|
+
}}
|
|
354
|
+
/>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### 3. React Context (Global State)
|
|
358
|
+
|
|
359
|
+
**Pros:**
|
|
360
|
+
- Share state across components
|
|
361
|
+
- Centralized state management
|
|
362
|
+
- No persistence setup needed
|
|
363
|
+
|
|
364
|
+
**Cons:**
|
|
365
|
+
- State lost on page refresh
|
|
366
|
+
- Not synced across tabs
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
const UIPreferencesContext = createContext();
|
|
370
|
+
|
|
371
|
+
function UIPreferencesProvider({ children }) {
|
|
372
|
+
const [aiPanelCollapsed, setAIPanelCollapsed] = useState(false);
|
|
373
|
+
return (
|
|
374
|
+
<UIPreferencesContext.Provider value={{ aiPanelCollapsed, setAIPanelCollapsed }}>
|
|
375
|
+
{children}
|
|
376
|
+
</UIPreferencesContext.Provider>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function MyComponent() {
|
|
381
|
+
const { aiPanelCollapsed, setAIPanelCollapsed } = useContext(UIPreferencesContext);
|
|
382
|
+
return (
|
|
383
|
+
<AIAgentPanel
|
|
384
|
+
isCollapsed={aiPanelCollapsed}
|
|
385
|
+
onCollapsedChange={setAIPanelCollapsed}
|
|
386
|
+
/>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### 4. URL Parameters (Deep Linking)
|
|
392
|
+
|
|
393
|
+
**Pros:**
|
|
394
|
+
- Shareable state via URL
|
|
395
|
+
- Bookmarkable
|
|
396
|
+
- Works with browser back/forward
|
|
397
|
+
|
|
398
|
+
**Cons:**
|
|
399
|
+
- Visible in URL
|
|
400
|
+
- Limited to URL-safe values
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
404
|
+
const collapsed = searchParams.get('panelCollapsed') === 'true';
|
|
405
|
+
|
|
406
|
+
const handleCollapsedChange = (isCollapsed: boolean) => {
|
|
407
|
+
setSearchParams({ panelCollapsed: String(isCollapsed) });
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
<AIAgentPanel
|
|
411
|
+
isCollapsed={collapsed}
|
|
412
|
+
onCollapsedChange={handleCollapsedChange}
|
|
413
|
+
/>
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## Best Practices
|
|
417
|
+
|
|
418
|
+
### 1. Use useCallback for Performance
|
|
419
|
+
|
|
420
|
+
Prevent unnecessary re-renders by memoizing the callback:
|
|
421
|
+
|
|
422
|
+
```tsx
|
|
423
|
+
const handleCollapsedChange = useCallback((isCollapsed: boolean) => {
|
|
424
|
+
setCollapsed(isCollapsed);
|
|
425
|
+
localStorage.setItem('aiPanelCollapsed', String(isCollapsed));
|
|
426
|
+
}, []);
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### 2. Initialize from Persistence Layer
|
|
430
|
+
|
|
431
|
+
Load the initial state from your persistence layer:
|
|
432
|
+
|
|
433
|
+
```tsx
|
|
434
|
+
const [collapsed, setCollapsed] = useState(() => {
|
|
435
|
+
// Initialize from localStorage
|
|
436
|
+
if (typeof window !== 'undefined') {
|
|
437
|
+
return localStorage.getItem('aiPanelCollapsed') === 'true';
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### 3. Handle Loading States
|
|
444
|
+
|
|
445
|
+
Show a loading state while fetching user preferences:
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
const { data: preferences, isLoading } = useUserPreferences();
|
|
449
|
+
|
|
450
|
+
if (isLoading) {
|
|
451
|
+
return <div>Loading...</div>;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<AIAgentPanel
|
|
456
|
+
isCollapsed={preferences?.aiPanelCollapsed ?? false}
|
|
457
|
+
onCollapsedChange={(isCollapsed) => {
|
|
458
|
+
updatePreferences({ aiPanelCollapsed: isCollapsed });
|
|
459
|
+
}}
|
|
460
|
+
/>
|
|
461
|
+
);
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### 4. Provide Fallback Values
|
|
465
|
+
|
|
466
|
+
Always provide a fallback value for the controlled state:
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
<AIAgentPanel
|
|
470
|
+
isCollapsed={preferences?.aiPanelCollapsed ?? false} // Fallback to false
|
|
471
|
+
onCollapsedChange={handleCollapsedChange}
|
|
472
|
+
/>
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### 5. Consider Mobile Behavior
|
|
476
|
+
|
|
477
|
+
Auto-collapse on mobile devices for better UX:
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
const [collapsed, setCollapsed] = useState(() => {
|
|
481
|
+
if (typeof window !== 'undefined') {
|
|
482
|
+
// Auto-collapse on mobile
|
|
483
|
+
return window.innerWidth < 768;
|
|
484
|
+
}
|
|
485
|
+
return false;
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## TypeScript Support
|
|
490
|
+
|
|
491
|
+
The component is fully typed with TypeScript:
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
interface AIAgentPanelProps {
|
|
495
|
+
// ... other props
|
|
496
|
+
|
|
497
|
+
/** Controlled collapse state */
|
|
498
|
+
isCollapsed?: boolean;
|
|
499
|
+
|
|
500
|
+
/** Callback when collapse state changes */
|
|
501
|
+
onCollapsedChange?: (isCollapsed: boolean) => void;
|
|
502
|
+
|
|
503
|
+
/** Initial collapse state (uncontrolled mode only) */
|
|
504
|
+
defaultCollapsed?: boolean;
|
|
505
|
+
|
|
506
|
+
/** Whether the panel can be collapsed */
|
|
507
|
+
collapsible?: boolean;
|
|
508
|
+
|
|
509
|
+
// ... other props
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## Testing
|
|
514
|
+
|
|
515
|
+
### Unit Tests
|
|
516
|
+
|
|
517
|
+
Test both controlled and uncontrolled modes:
|
|
518
|
+
|
|
519
|
+
```tsx
|
|
520
|
+
describe('AIAgentPanel collapse state', () => {
|
|
521
|
+
it('should work in uncontrolled mode', () => {
|
|
522
|
+
render(<AIAgentPanel defaultCollapsed={false} {...requiredProps} />);
|
|
523
|
+
// Test default behavior
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should work in controlled mode', () => {
|
|
527
|
+
const onCollapsedChange = jest.fn();
|
|
528
|
+
const { rerender } = render(
|
|
529
|
+
<AIAgentPanel
|
|
530
|
+
isCollapsed={false}
|
|
531
|
+
onCollapsedChange={onCollapsedChange}
|
|
532
|
+
{...requiredProps}
|
|
533
|
+
/>
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
// Click collapse button
|
|
537
|
+
fireEvent.click(screen.getByRole('button', { name: /collapse/i }));
|
|
538
|
+
|
|
539
|
+
// Callback should be called
|
|
540
|
+
expect(onCollapsedChange).toHaveBeenCalledWith(true);
|
|
541
|
+
|
|
542
|
+
// Rerender with new state
|
|
543
|
+
rerender(
|
|
544
|
+
<AIAgentPanel
|
|
545
|
+
isCollapsed={true}
|
|
546
|
+
onCollapsedChange={onCollapsedChange}
|
|
547
|
+
{...requiredProps}
|
|
548
|
+
/>
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Panel should be collapsed
|
|
552
|
+
expect(screen.getByRole('complementary')).toHaveClass('collapsed');
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Integration Tests
|
|
558
|
+
|
|
559
|
+
Test persistence across navigation:
|
|
560
|
+
|
|
561
|
+
```tsx
|
|
562
|
+
describe('AIAgentPanel persistence', () => {
|
|
563
|
+
it('should persist collapse state across page navigations', () => {
|
|
564
|
+
const { rerender } = render(<App />);
|
|
565
|
+
|
|
566
|
+
// Collapse the panel
|
|
567
|
+
fireEvent.click(screen.getByRole('button', { name: /collapse/i }));
|
|
568
|
+
|
|
569
|
+
// Navigate to another page
|
|
570
|
+
fireEvent.click(screen.getByText('Profile'));
|
|
571
|
+
|
|
572
|
+
// Panel should still be collapsed
|
|
573
|
+
expect(screen.getByRole('complementary')).toHaveClass('collapsed');
|
|
574
|
+
|
|
575
|
+
// Navigate back
|
|
576
|
+
fireEvent.click(screen.getByText('Dashboard'));
|
|
577
|
+
|
|
578
|
+
// Panel should still be collapsed
|
|
579
|
+
expect(screen.getByRole('complementary')).toHaveClass('collapsed');
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## Troubleshooting
|
|
585
|
+
|
|
586
|
+
### Issue: Panel doesn't collapse when clicking the button
|
|
587
|
+
|
|
588
|
+
**Cause:** Using controlled mode without providing `onCollapsedChange`.
|
|
589
|
+
|
|
590
|
+
**Solution:** Provide both `isCollapsed` and `onCollapsedChange`:
|
|
591
|
+
|
|
592
|
+
```tsx
|
|
593
|
+
// ❌ Wrong - missing onCollapsedChange
|
|
594
|
+
<AIAgentPanel isCollapsed={collapsed} />
|
|
595
|
+
|
|
596
|
+
// ✅ Correct
|
|
597
|
+
<AIAgentPanel
|
|
598
|
+
isCollapsed={collapsed}
|
|
599
|
+
onCollapsedChange={setCollapsed}
|
|
600
|
+
/>
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Issue: State resets on page navigation
|
|
604
|
+
|
|
605
|
+
**Cause:** Using uncontrolled mode without persistence.
|
|
606
|
+
|
|
607
|
+
**Solution:** Switch to controlled mode with localStorage:
|
|
608
|
+
|
|
609
|
+
```tsx
|
|
610
|
+
const [collapsed, setCollapsed] = useState(() => {
|
|
611
|
+
return localStorage.getItem('aiPanelCollapsed') === 'true';
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
const handleCollapsedChange = (isCollapsed: boolean) => {
|
|
615
|
+
setCollapsed(isCollapsed);
|
|
616
|
+
localStorage.setItem('aiPanelCollapsed', String(isCollapsed));
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
<AIAgentPanel
|
|
620
|
+
isCollapsed={collapsed}
|
|
621
|
+
onCollapsedChange={handleCollapsedChange}
|
|
622
|
+
/>
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### Issue: Warning about conflicting props
|
|
626
|
+
|
|
627
|
+
**Cause:** Providing both `isCollapsed` and `defaultCollapsed`.
|
|
628
|
+
|
|
629
|
+
**Solution:** Remove `defaultCollapsed` when using controlled mode:
|
|
630
|
+
|
|
631
|
+
```tsx
|
|
632
|
+
// ❌ Wrong - conflicting props
|
|
633
|
+
<AIAgentPanel
|
|
634
|
+
isCollapsed={collapsed}
|
|
635
|
+
onCollapsedChange={setCollapsed}
|
|
636
|
+
defaultCollapsed={false} // Remove this
|
|
637
|
+
/>
|
|
638
|
+
|
|
639
|
+
// ✅ Correct
|
|
640
|
+
<AIAgentPanel
|
|
641
|
+
isCollapsed={collapsed}
|
|
642
|
+
onCollapsedChange={setCollapsed}
|
|
643
|
+
/>
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
## See Also
|
|
647
|
+
|
|
648
|
+
- [AIAgentPanel Documentation](./AIAGENTPANEL.md)
|
|
649
|
+
- [Examples](../examples/controlled-collapse-example.tsx)
|
|
650
|
+
- [Demo App](../examples/demo-app/src/App.tsx)
|
|
651
|
+
|