@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.
@@ -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
+
@@ -862,3 +862,5 @@ Minimal interface embedded in another application.
862
862
 
863
863
  If you have questions or suggestions about conversation history configuration, please open an issue on GitHub or contact support.
864
864
 
865
+
866
+
@@ -252,3 +252,5 @@ Potential improvements for future iterations:
252
252
  - The implementation is consistent across both modern (AIChatPanel) and legacy (ChatPanel) components
253
253
  - Error state is properly synchronized with loading state to prevent UI inconsistencies
254
254
 
255
+
256
+
@@ -130,3 +130,5 @@ The implementation is straightforward:
130
130
 
131
131
  No configuration needed, works automatically! 🎉
132
132
 
133
+
134
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hef2024/llmasaservice-ui",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "description": "Prebuilt UI components for LLMAsAService.io",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",