@djangocfg/layouts 2.1.227 → 2.1.228

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.
Files changed (97) hide show
  1. package/README.md +3 -17
  2. package/package.json +18 -18
  3. package/src/components/errors/ErrorLayout.tsx +2 -2
  4. package/src/components/errors/ErrorsTracker/index.ts +1 -0
  5. package/src/components/errors/ErrorsTracker/utils/formatters.ts +23 -1
  6. package/src/hooks/useLogout.ts +9 -12
  7. package/src/layouts/AppLayout/AppLayout.tsx +20 -8
  8. package/src/layouts/AppLayout/BaseApp.tsx +5 -28
  9. package/src/layouts/AuthLayout/AuthLayout.tsx +51 -22
  10. package/src/layouts/AuthLayout/README.md +78 -0
  11. package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +2 -2
  12. package/src/layouts/AuthLayout/components/shared/AuthError.tsx +10 -2
  13. package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +2 -2
  14. package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +3 -2
  15. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +4 -1
  16. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +2 -2
  17. package/src/layouts/AuthLayout/components/shared/index.ts +0 -2
  18. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +25 -80
  19. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +8 -13
  20. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +2 -2
  21. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +2 -2
  22. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +2 -2
  23. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +61 -42
  24. package/src/layouts/AuthLayout/context.tsx +0 -2
  25. package/src/layouts/AuthLayout/index.ts +9 -6
  26. package/src/layouts/AuthLayout/styles/auth.css +265 -120
  27. package/src/layouts/AuthLayout/types.ts +60 -7
  28. package/src/layouts/ProfileLayout/.claude/.sidecar/activity.jsonl +2 -0
  29. package/src/layouts/ProfileLayout/.claude/.sidecar/history/2026-03-15.md +35 -0
  30. package/src/layouts/ProfileLayout/.claude/.sidecar/review.md +35 -0
  31. package/src/layouts/ProfileLayout/.claude/.sidecar/scan.log +3 -0
  32. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-001.md +18 -0
  33. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-002.md +19 -0
  34. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-003.md +18 -0
  35. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-004.md +18 -0
  36. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-005.md +18 -0
  37. package/src/layouts/ProfileLayout/.claude/.sidecar/usage.json +5 -0
  38. package/src/layouts/ProfileLayout/ProfileLayout.tsx +52 -403
  39. package/src/layouts/ProfileLayout/components/ActionButton.tsx +38 -0
  40. package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +109 -148
  41. package/src/layouts/ProfileLayout/components/EditableField.tsx +119 -0
  42. package/src/layouts/ProfileLayout/components/Section.tsx +22 -0
  43. package/src/layouts/ProfileLayout/components/index.ts +4 -1
  44. package/src/layouts/ProfileLayout/context.tsx +31 -0
  45. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +2 -2
  46. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +2 -2
  47. package/src/layouts/_components/UserMenu.tsx +2 -2
  48. package/src/layouts/types/README.md +0 -20
  49. package/src/layouts/types/index.ts +2 -2
  50. package/src/layouts/types/layout.types.ts +2 -5
  51. package/src/layouts/types/providers.types.ts +0 -27
  52. package/src/snippets/AuthDialog/AuthDialog.tsx +2 -2
  53. package/src/snippets/Breadcrumbs.tsx +2 -2
  54. package/src/snippets/index.ts +0 -67
  55. package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +0 -56
  56. package/src/snippets/McpChat/README.md +0 -441
  57. package/src/snippets/McpChat/components/AIChatWidget.tsx +0 -361
  58. package/src/snippets/McpChat/components/AskAIButton.tsx +0 -92
  59. package/src/snippets/McpChat/components/ChatMessages.tsx +0 -138
  60. package/src/snippets/McpChat/components/ChatPanel.tsx +0 -131
  61. package/src/snippets/McpChat/components/ChatSidebar.tsx +0 -156
  62. package/src/snippets/McpChat/components/ChatWidget.tsx +0 -115
  63. package/src/snippets/McpChat/components/MessageBubble.tsx +0 -142
  64. package/src/snippets/McpChat/components/MessageInput.tsx +0 -140
  65. package/src/snippets/McpChat/components/index.ts +0 -24
  66. package/src/snippets/McpChat/config.ts +0 -94
  67. package/src/snippets/McpChat/context/AIChatContext.tsx +0 -327
  68. package/src/snippets/McpChat/context/ChatContext.tsx +0 -361
  69. package/src/snippets/McpChat/context/index.ts +0 -7
  70. package/src/snippets/McpChat/hooks/index.ts +0 -6
  71. package/src/snippets/McpChat/hooks/useAIChat.ts +0 -503
  72. package/src/snippets/McpChat/hooks/useChatLayout.ts +0 -442
  73. package/src/snippets/McpChat/hooks/useMcpChat.ts +0 -90
  74. package/src/snippets/McpChat/index.ts +0 -79
  75. package/src/snippets/McpChat/types.ts +0 -189
  76. package/src/snippets/PWAInstall/@docs/README.md +0 -92
  77. package/src/snippets/PWAInstall/@docs/research/ios-android-install-flows.md +0 -576
  78. package/src/snippets/PWAInstall/README.md +0 -235
  79. package/src/snippets/PWAInstall/components/A2HSHint.tsx +0 -236
  80. package/src/snippets/PWAInstall/components/DesktopGuide.tsx +0 -234
  81. package/src/snippets/PWAInstall/components/IOSGuide.tsx +0 -29
  82. package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +0 -103
  83. package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +0 -103
  84. package/src/snippets/PWAInstall/components/PWAPageResumeManager.tsx +0 -33
  85. package/src/snippets/PWAInstall/context/InstallContext.tsx +0 -102
  86. package/src/snippets/PWAInstall/hooks/useInstallPrompt.ts +0 -168
  87. package/src/snippets/PWAInstall/hooks/useIsPWA.ts +0 -116
  88. package/src/snippets/PWAInstall/hooks/usePWAPageResume.ts +0 -163
  89. package/src/snippets/PWAInstall/index.ts +0 -80
  90. package/src/snippets/PWAInstall/types/components.ts +0 -95
  91. package/src/snippets/PWAInstall/types/config.ts +0 -29
  92. package/src/snippets/PWAInstall/types/index.ts +0 -26
  93. package/src/snippets/PWAInstall/types/install.ts +0 -38
  94. package/src/snippets/PWAInstall/types/platform.ts +0 -29
  95. package/src/snippets/PWAInstall/utils/localStorage.ts +0 -181
  96. package/src/snippets/PWAInstall/utils/logger.ts +0 -149
  97. package/src/snippets/PWAInstall/utils/platform.ts +0 -151
@@ -1,361 +0,0 @@
1
- 'use client';
2
-
3
- import { Zap } from 'lucide-react';
4
- import React from 'react';
5
-
6
- import { Button, Portal } from '@djangocfg/ui-core';
7
-
8
- import {
9
- AIChatProvider, useAIChatContext, useAIChatContextOptional
10
- } from '../context/AIChatContext';
11
- import { useChatLayout } from '../hooks/useChatLayout';
12
- import { ChatWidgetConfig, getMcpEndpoints} from '../types';
13
- import { ChatPanel } from './ChatPanel';
14
- import { ChatSidebar } from './ChatSidebar';
15
-
16
- // CSS for game-quality multi-layer animated border with smooth color flow
17
- const fabAnimationStyles = `
18
- @keyframes rotate-gradient {
19
- 0% { transform: rotate(0deg); }
20
- 100% { transform: rotate(360deg); }
21
- }
22
-
23
- @keyframes rotate-gradient-reverse {
24
- 0% { transform: rotate(360deg); }
25
- 100% { transform: rotate(0deg); }
26
- }
27
-
28
- @keyframes color-shift-glow {
29
- 0%, 100% {
30
- box-shadow:
31
- 0 0 20px rgba(251, 191, 36, 0.5),
32
- 0 0 40px rgba(168, 85, 247, 0.3),
33
- 0 0 60px rgba(20, 184, 166, 0.2);
34
- }
35
- 33% {
36
- box-shadow:
37
- 0 0 20px rgba(168, 85, 247, 0.5),
38
- 0 0 40px rgba(20, 184, 166, 0.3),
39
- 0 0 60px rgba(251, 191, 36, 0.2);
40
- }
41
- 66% {
42
- box-shadow:
43
- 0 0 20px rgba(20, 184, 166, 0.5),
44
- 0 0 40px rgba(236, 72, 153, 0.3),
45
- 0 0 60px rgba(168, 85, 247, 0.2);
46
- }
47
- }
48
-
49
- @keyframes icon-pulse {
50
- 0%, 100% {
51
- opacity: 1;
52
- transform: scale(1);
53
- filter: drop-shadow(0 0 4px rgba(251, 191, 36, 0.7));
54
- }
55
- 50% {
56
- opacity: 0.85;
57
- transform: scale(1.15);
58
- filter: drop-shadow(0 0 12px rgba(251, 191, 36, 1));
59
- }
60
- }
61
-
62
- @keyframes border-pulse {
63
- 0%, 100% {
64
- opacity: 1;
65
- filter: blur(0px);
66
- }
67
- 50% {
68
- opacity: 0.85;
69
- filter: blur(0.5px);
70
- }
71
- }
72
-
73
- @keyframes inner-glow-pulse {
74
- 0%, 100% {
75
- box-shadow:
76
- inset 0 0 15px rgba(251, 191, 36, 0.3),
77
- inset 0 0 25px rgba(168, 85, 247, 0.2);
78
- }
79
- 50% {
80
- box-shadow:
81
- inset 0 0 20px rgba(168, 85, 247, 0.35),
82
- inset 0 0 30px rgba(20, 184, 166, 0.25);
83
- }
84
- }
85
-
86
- @keyframes fab-entrance {
87
- 0% {
88
- transform: scale(0);
89
- }
90
- 50% {
91
- transform: scale(1.08);
92
- }
93
- 70% {
94
- transform: scale(0.98);
95
- }
96
- 85% {
97
- transform: scale(1.02);
98
- }
99
- 100% {
100
- transform: scale(1);
101
- }
102
- }
103
-
104
- @keyframes fab-glow-entrance {
105
- 0% {
106
- box-shadow: 0 0 0 rgba(251, 191, 36, 0);
107
- }
108
- 40% {
109
- box-shadow:
110
- 0 0 25px rgba(251, 191, 36, 0.6),
111
- 0 0 50px rgba(168, 85, 247, 0.4),
112
- 0 0 75px rgba(20, 184, 166, 0.25);
113
- }
114
- 100% {
115
- box-shadow:
116
- 0 0 20px rgba(251, 191, 36, 0.5),
117
- 0 0 40px rgba(168, 85, 247, 0.3),
118
- 0 0 60px rgba(20, 184, 166, 0.2);
119
- }
120
- }
121
- `;
122
-
123
- export interface AIChatWidgetProps extends ChatWidgetConfig {
124
- /** Custom class name for the container */
125
- className?: string;
126
- /** Enable streaming responses (default: true) */
127
- enableStreaming?: boolean;
128
- }
129
-
130
- /**
131
- * Internal AI chat widget that uses context
132
- */
133
- const AIChatWidgetInternal = React.memo<{ className?: string }>(({ className }) => {
134
- const { config, displayMode, openChat, isMobile } = useAIChatContext();
135
-
136
- // Use layout hook for consistent positioning
137
- const { getFabStyles, getFloatingStyles } = useChatLayout();
138
-
139
- const position = config.position || 'bottom-right';
140
- const fabStyles = getFabStyles(position);
141
- const floatingStyles = getFloatingStyles(position);
142
-
143
- // Mode: closed - just show FAB with game-quality multi-layer animated border
144
- if (displayMode === 'closed') {
145
- return (
146
- <Portal>
147
- <style>{fabAnimationStyles}</style>
148
- <div style={fabStyles} className={className || ''}>
149
- {/* Outer glow container with entrance and color-shifting animations */}
150
- <div
151
- className="relative rounded-full"
152
- style={{
153
- width: '68px',
154
- height: '68px',
155
- overflow: 'hidden',
156
- animation: 'fab-entrance 0.6s cubic-bezier(0.34, 1.45, 0.64, 1) 0s 1 normal forwards, fab-glow-entrance 0.8s ease-out 0s 1 normal forwards, color-shift-glow 8s ease-in-out 0.6s infinite',
157
- }}
158
- >
159
- {/* Border container - multiple layers for depth */}
160
- <div
161
- className="absolute rounded-full"
162
- style={{
163
- inset: '0',
164
- overflow: 'hidden',
165
- }}
166
- >
167
- {/* Layer 1: Base smooth gradient with more color stops */}
168
- <div
169
- className="absolute rounded-full"
170
- style={{
171
- inset: '0',
172
- background: `conic-gradient(
173
- from 0deg,
174
- rgba(251, 191, 36, 1) 0%,
175
- rgba(251, 191, 36, 0.7) 8%,
176
- rgba(251, 191, 36, 0) 15%,
177
- rgba(168, 85, 247, 0) 20%,
178
- rgba(168, 85, 247, 0.7) 28%,
179
- rgba(168, 85, 247, 1) 35%,
180
- rgba(168, 85, 247, 0.7) 42%,
181
- rgba(168, 85, 247, 0) 50%,
182
- rgba(20, 184, 166, 0) 55%,
183
- rgba(20, 184, 166, 0.7) 63%,
184
- rgba(20, 184, 166, 1) 70%,
185
- rgba(20, 184, 166, 0.7) 77%,
186
- rgba(20, 184, 166, 0) 85%,
187
- rgba(236, 72, 153, 0) 88%,
188
- rgba(236, 72, 153, 0.7) 93%,
189
- rgba(236, 72, 153, 1) 97%,
190
- rgba(251, 191, 36, 1) 100%
191
- )`,
192
- animation: 'rotate-gradient 7s linear infinite, border-pulse 4s ease-in-out infinite',
193
- filter: 'blur(1px)',
194
- opacity: 0.95,
195
- }}
196
- />
197
-
198
- {/* Layer 2: Secondary gradient (counter-clockwise) - stronger */}
199
- <div
200
- className="absolute rounded-full"
201
- style={{
202
- inset: '1px',
203
- background: `conic-gradient(
204
- from 180deg,
205
- rgba(168, 85, 247, 0.85) 0%,
206
- rgba(168, 85, 247, 0.5) 10%,
207
- rgba(168, 85, 247, 0) 20%,
208
- rgba(20, 184, 166, 0) 30%,
209
- rgba(20, 184, 166, 0.5) 40%,
210
- rgba(20, 184, 166, 0.85) 50%,
211
- rgba(20, 184, 166, 0.5) 60%,
212
- rgba(20, 184, 166, 0) 70%,
213
- rgba(251, 191, 36, 0) 75%,
214
- rgba(251, 191, 36, 0.5) 85%,
215
- rgba(251, 191, 36, 0.85) 95%,
216
- rgba(168, 85, 247, 0.85) 100%
217
- )`,
218
- animation: 'rotate-gradient-reverse 9s linear infinite',
219
- filter: 'blur(0.75px)',
220
- opacity: 0.75,
221
- }}
222
- />
223
-
224
- {/* Inner mask with glowing edge */}
225
- <div
226
- className="absolute rounded-full bg-background"
227
- style={{
228
- inset: '4px',
229
- animation: 'inner-glow-pulse 5s ease-in-out infinite',
230
- }}
231
- />
232
-
233
- {/* Main FAB button */}
234
- <Button
235
- onClick={openChat}
236
- variant="ghost"
237
- className="absolute rounded-full hover:scale-105 transition-all duration-300 bg-background/80 hover:bg-background/95 border-0 backdrop-blur-sm"
238
- style={{
239
- inset: '2.5px',
240
- width: 'auto',
241
- height: 'auto',
242
- }}
243
- >
244
- <Zap
245
- className="h-6 w-6"
246
- style={{
247
- animation: 'icon-pulse 2.5s ease-in-out infinite',
248
- color: '#fbbf24',
249
- fill: '#fbbf24',
250
- }}
251
- />
252
- </Button>
253
- </div>
254
- </div>
255
- </div>
256
- </Portal>
257
- );
258
- }
259
-
260
- // Mode: sidebar - full-height panel on the right (desktop only)
261
- if (displayMode === 'sidebar') {
262
- return (
263
- <Portal>
264
- <ChatSidebar />
265
- </Portal>
266
- );
267
- }
268
-
269
- // Mode: floating - fullscreen on mobile (via Portal), floating panel on desktop
270
- if (isMobile) {
271
- return (
272
- <Portal>
273
- <div
274
- className="z-[400] overflow-hidden"
275
- style={{
276
- position: 'fixed',
277
- top: 0,
278
- left: 0,
279
- right: 0,
280
- bottom: 0,
281
- width: '100vw',
282
- height: '100dvh',
283
- }}
284
- >
285
- <ChatPanel />
286
- </div>
287
- </Portal>
288
- );
289
- }
290
-
291
- return (
292
- <Portal>
293
- <div style={floatingStyles} className={className || ''}>
294
- <ChatPanel />
295
- </div>
296
- </Portal>
297
- );
298
- });
299
-
300
- AIChatWidgetInternal.displayName = 'AIChatWidgetInternal';
301
-
302
- /**
303
- * AI Chat Widget component
304
- *
305
- * AI-powered documentation assistant with streaming support.
306
- * Uses Mastra agent backend for intelligent responses.
307
- *
308
- * Can be used in two ways:
309
- * 1. Standalone (wraps itself in AIChatProvider)
310
- * 2. Inside an AIChatProvider (uses context directly)
311
- *
312
- * @example
313
- * ```tsx
314
- * // Standalone usage (always uses production API)
315
- * <AIChatWidget />
316
- *
317
- * // Auto-detect environment (dev/prod)
318
- * <AIChatWidget autoDetectEnvironment={true} />
319
- *
320
- * // With provider for custom control
321
- * <AIChatProvider apiEndpoint="/api/ai/chat">
322
- * <MyApp />
323
- * <AIChatWidget />
324
- * </AIChatProvider>
325
- * ```
326
- */
327
- export const AIChatWidget: React.FC<AIChatWidgetProps> = ({
328
- apiEndpoint,
329
- title = 'DjangoCFG AI',
330
- placeholder = 'Ask about DjangoCFG...',
331
- greeting = "Hi! I'm your DjangoCFG AI assistant powered by GPT. Ask me anything about configuration, features, or how to use the library.",
332
- position = 'bottom-right',
333
- variant = 'default',
334
- className,
335
- enableStreaming = true,
336
- autoDetectEnvironment = false,
337
- }) => {
338
- // Check if we're inside an AIChatProvider
339
- const existingContext = useAIChatContextOptional();
340
-
341
- // If already in context, use internal widget directly
342
- if (existingContext) {
343
- return <AIChatWidgetInternal className={className} />;
344
- }
345
-
346
- // Determine API endpoint: use provided, or get from config based on autoDetect
347
- const finalApiEndpoint = apiEndpoint || getMcpEndpoints(autoDetectEnvironment).chat;
348
-
349
- // Otherwise, wrap in provider
350
- return (
351
- <AIChatProvider
352
- apiEndpoint={finalApiEndpoint}
353
- config={{ title, placeholder, greeting, position, variant, autoDetectEnvironment }}
354
- enableStreaming={enableStreaming}
355
- >
356
- <AIChatWidgetInternal className={className} />
357
- </AIChatProvider>
358
- );
359
- };
360
-
361
- AIChatWidget.displayName = 'AIChatWidget';
@@ -1,92 +0,0 @@
1
- 'use client';
2
-
3
- import { Bot } from 'lucide-react';
4
-
5
- import { Button, ButtonProps} from '@djangocfg/ui-core';
6
-
7
- import { useMcpChat } from '../hooks/useMcpChat';
8
-
9
- import type { McpChatEventDetail } from '../types';
10
-
11
- export interface AskAIButtonProps extends Omit<ButtonProps, 'onClick'> {
12
- /** Message to send to AI */
13
- message: string;
14
- /** Additional context data */
15
- contextData?: Record<string, any>;
16
- /** Source component name */
17
- source?: string;
18
- /** Auto-send message (default: true) */
19
- autoSend?: boolean;
20
- /** Show icon (default: true) */
21
- showIcon?: boolean;
22
- /** Callback after sending */
23
- onSent?: () => void;
24
- }
25
-
26
- /**
27
- * Universal AI chat trigger button
28
- *
29
- * @example Basic usage
30
- * ```tsx
31
- * <AskAIButton message="Explain this feature">
32
- * Explain this
33
- * </AskAIButton>
34
- * ```
35
- *
36
- * @example With context
37
- * ```tsx
38
- * <AskAIButton
39
- * message="Why is this failing?"
40
- * contextData={{ error: error.stack }}
41
- * source="ErrorBoundary"
42
- * >
43
- * Ask AI
44
- * </AskAIButton>
45
- * ```
46
- */
47
- export function AskAIButton({
48
- message,
49
- contextData,
50
- source,
51
- autoSend = true,
52
- showIcon = true,
53
- onSent,
54
- children = 'Ask AI',
55
- variant = 'outline',
56
- size = 'default',
57
- className,
58
- ...buttonProps
59
- }: AskAIButtonProps) {
60
- const { sendToChat } = useMcpChat();
61
-
62
- const handleClick = () => {
63
- const detail: McpChatEventDetail = {
64
- message,
65
- autoSend,
66
- // No displayMode - chat will use remembered mode automatically
67
- };
68
-
69
- if (contextData || source) {
70
- detail.context = {
71
- data: contextData,
72
- source,
73
- };
74
- }
75
-
76
- sendToChat(detail);
77
- onSent?.();
78
- };
79
-
80
- return (
81
- <Button
82
- onClick={handleClick}
83
- variant={variant}
84
- size={size}
85
- className={className}
86
- {...buttonProps}
87
- >
88
- {showIcon && <Bot className="h-4 w-4 mr-2" />}
89
- {children}
90
- </Button>
91
- );
92
- }
@@ -1,138 +0,0 @@
1
- 'use client';
2
-
3
- import { Bot, MessageSquare, StopCircle } from 'lucide-react';
4
- import React, { forwardRef, useImperativeHandle, useRef } from 'react';
5
-
6
- import { Button } from '@djangocfg/ui-core';
7
-
8
- import { MessageBubble } from './MessageBubble';
9
-
10
- import type { AIChatMessage } from '../types';
11
-
12
- /**
13
- * ChatMessages imperative handle
14
- */
15
- export interface ChatMessagesHandle {
16
- scrollToBottom: (instant?: boolean) => void;
17
- scrollToLastMessage: (instant?: boolean) => void;
18
- }
19
-
20
- export interface ChatMessagesProps {
21
- /** Messages to display */
22
- messages: AIChatMessage[];
23
- /** Whether loading/streaming is in progress */
24
- isLoading: boolean;
25
- /** Greeting to show when no messages */
26
- greeting?: string;
27
- /** Callback to stop streaming */
28
- onStopStreaming?: () => void;
29
- /** Use compact layout (smaller bubbles) */
30
- isCompact?: boolean;
31
- /** Use larger icon for greeting (sidebar mode) */
32
- largeGreetingIcon?: boolean;
33
- /** Custom greeting icon */
34
- greetingIcon?: 'bot' | 'message';
35
- /** Custom greeting title */
36
- greetingTitle?: string;
37
- }
38
-
39
- /**
40
- * ChatMessages - Shared component for displaying chat messages
41
- *
42
- * Uses flex-col-reverse for natural bottom scroll behavior:
43
- * - Scroll position 0 = visual bottom
44
- * - New content naturally stays at bottom
45
- * - No complex JS scroll management needed
46
- */
47
- export const ChatMessages = forwardRef<ChatMessagesHandle, ChatMessagesProps>(
48
- (
49
- {
50
- messages,
51
- isLoading,
52
- greeting,
53
- onStopStreaming,
54
- isCompact = false,
55
- largeGreetingIcon = false,
56
- greetingIcon = 'bot',
57
- greetingTitle,
58
- },
59
- ref
60
- ) => {
61
- const scrollContainerRef = useRef<HTMLDivElement>(null);
62
-
63
- // Expose scroll methods via ref (for compatibility)
64
- useImperativeHandle(ref, () => ({
65
- scrollToBottom: () => {
66
- // With flex-col-reverse, scroll to top = visual bottom
67
- scrollContainerRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
68
- },
69
- scrollToLastMessage: () => {
70
- scrollContainerRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
71
- },
72
- }), []);
73
-
74
- // No manual scroll effects needed - flex-col-reverse handles it naturally
75
-
76
- const GreetingIcon = greetingIcon === 'message' ? MessageSquare : Bot;
77
- const iconSize = largeGreetingIcon ? { container: '64px', icon: 'h-8 w-8' } : { container: '48px', icon: 'h-6 w-6' };
78
- const padding = largeGreetingIcon ? 'py-12' : 'py-8';
79
-
80
- return (
81
- <div ref={scrollContainerRef} className="h-full w-full overflow-y-auto flex flex-col-reverse">
82
- <div className={`${isCompact ? 'p-3' : 'p-4'} space-y-4 max-w-full overflow-x-hidden`}>
83
- {/* Greeting */}
84
- {messages.length === 0 && greeting && (
85
- <div className={`text-center ${padding}`}>
86
- <div
87
- className="mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center"
88
- style={{ width: iconSize.container, height: iconSize.container }}
89
- >
90
- <GreetingIcon className={`${iconSize.icon} text-primary`} />
91
- </div>
92
- {greetingTitle && (
93
- <h4 className="font-medium mb-2">{greetingTitle}</h4>
94
- )}
95
- <p className={`text-sm text-muted-foreground ${largeGreetingIcon ? 'max-w-[300px]' : 'max-w-[280px]'} mx-auto`}>
96
- {greeting}
97
- </p>
98
- </div>
99
- )}
100
-
101
- {/* Messages */}
102
- {messages.map((message) => (
103
- <div key={message.id} data-message-bubble>
104
- <MessageBubble message={message} isCompact={isCompact} />
105
- </div>
106
- ))}
107
-
108
- {/* Loading indicator with stop button */}
109
- {isLoading && messages.length > 0 && (
110
- <div className="flex items-center justify-between text-muted-foreground text-sm">
111
- <div className="flex items-center gap-2">
112
- <div className="flex gap-1">
113
- <span className="animate-bounce" style={{ animationDelay: '0ms' }}>.</span>
114
- <span className="animate-bounce" style={{ animationDelay: '150ms' }}>.</span>
115
- <span className="animate-bounce" style={{ animationDelay: '300ms' }}>.</span>
116
- </div>
117
- <span>Generating response...</span>
118
- </div>
119
- {onStopStreaming && (
120
- <Button
121
- variant="ghost"
122
- size="sm"
123
- onClick={onStopStreaming}
124
- className="h-6 px-2 text-xs"
125
- >
126
- <StopCircle className="h-3 w-3 mr-1" />
127
- Stop
128
- </Button>
129
- )}
130
- </div>
131
- )}
132
- </div>
133
- </div>
134
- );
135
- }
136
- );
137
-
138
- ChatMessages.displayName = 'ChatMessages';