@djangocfg/layouts 2.1.226 → 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,131 +0,0 @@
1
- 'use client';
2
-
3
- import { Bot, PanelRight, RotateCcw, X } from 'lucide-react';
4
- import React, { useMemo } from 'react';
5
-
6
- import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
7
- import { Button, Card, CardContent, CardFooter, CardHeader } from '@djangocfg/ui-core';
8
-
9
- import { useAIChatContext } from '../context/AIChatContext';
10
- import { ChatMessages } from './ChatMessages';
11
- import { AIMessageInput } from './MessageInput';
12
-
13
- export const ChatPanel = React.memo(() => {
14
- const {
15
- messages,
16
- isLoading,
17
- config,
18
- isMobile,
19
- sendMessage,
20
- closeChat,
21
- setDisplayMode,
22
- stopStreaming,
23
- clearMessages,
24
- } = useAIChatContext();
25
- const t = useTypedT<I18nTranslations>();
26
-
27
- const labels = useMemo(() => ({
28
- aiAssistant: t('layouts.chat.aiAssistant'),
29
- defaultTitle: t('layouts.chat.defaultTitle'),
30
- newChat: t('layouts.chat.newChat'),
31
- closeChat: t('layouts.chat.closeChat'),
32
- switchToSidebar: t('layouts.chat.switchToSidebar'),
33
- }), [t]);
34
-
35
- // Mobile: fullscreen, Desktop: floating panel
36
- const panelStyles: React.CSSProperties = isMobile
37
- ? {
38
- position: 'absolute',
39
- top: 0,
40
- left: 0,
41
- right: 0,
42
- bottom: 0,
43
- width: '100%',
44
- height: '100%',
45
- maxHeight: '100dvh',
46
- borderRadius: 0,
47
- display: 'flex',
48
- flexDirection: 'column',
49
- margin: 0,
50
- border: 'none',
51
- }
52
- : {
53
- width: '380px',
54
- height: '520px',
55
- maxHeight: 'calc(100vh - 100px)',
56
- };
57
-
58
- return (
59
- <Card
60
- className={`flex flex-col ${isMobile ? 'rounded-none border-0 shadow-none' : 'shadow-2xl border-border/50'}`}
61
- style={panelStyles}
62
- >
63
- {/* Header */}
64
- <CardHeader className="flex flex-row items-center justify-between p-3 border-b">
65
- <div className="flex items-center gap-2">
66
- <div
67
- className="rounded-full bg-primary/10 flex items-center justify-center"
68
- style={{ width: '32px', height: '32px' }}
69
- >
70
- <Bot className="h-4 w-4 text-primary" />
71
- </div>
72
- <div>
73
- <h3 className="font-semibold text-sm">{config.title || labels.defaultTitle}</h3>
74
- <p className="text-xs text-muted-foreground">{labels.aiAssistant}</p>
75
- </div>
76
- </div>
77
- <div className="flex gap-1">
78
- {messages.length > 0 && (
79
- <Button
80
- variant="ghost"
81
- size="icon"
82
- className="h-8 w-8"
83
- onClick={clearMessages}
84
- title={labels.newChat}
85
- >
86
- <RotateCcw className="h-4 w-4" />
87
- </Button>
88
- )}
89
- {/* Sidebar mode button - only on desktop */}
90
- {!isMobile && (
91
- <Button
92
- variant="ghost"
93
- size="icon"
94
- className="h-8 w-8"
95
- onClick={() => setDisplayMode('sidebar')}
96
- title={labels.switchToSidebar}
97
- >
98
- <PanelRight className="h-4 w-4" />
99
- </Button>
100
- )}
101
- <Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title={labels.closeChat}>
102
- <X className="h-4 w-4" />
103
- </Button>
104
- </div>
105
- </CardHeader>
106
-
107
- {/* Messages */}
108
- <CardContent className="flex-1 p-0 overflow-hidden">
109
- <ChatMessages
110
- messages={messages}
111
- isLoading={isLoading}
112
- greeting={config.greeting}
113
- onStopStreaming={stopStreaming}
114
- isCompact
115
- greetingIcon="bot"
116
- />
117
- </CardContent>
118
-
119
- {/* Input */}
120
- <CardFooter className="p-3 border-t">
121
- <AIMessageInput
122
- onSend={sendMessage}
123
- isLoading={isLoading}
124
- placeholder={config.placeholder}
125
- />
126
- </CardFooter>
127
- </Card>
128
- );
129
- });
130
-
131
- ChatPanel.displayName = 'ChatPanel';
@@ -1,156 +0,0 @@
1
- 'use client';
2
-
3
- import { Bot, GripVertical, PanelRightClose, RotateCcw, X } from 'lucide-react';
4
- import React, { useEffect, useMemo } from 'react';
5
-
6
- import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
7
- import { Button } from '@djangocfg/ui-core';
8
-
9
- import { useAIChatContext } from '../context/AIChatContext';
10
- import { useChatLayout } from '../hooks/useChatLayout';
11
- import { ChatMessages } from './ChatMessages';
12
- import { AIMessageInput } from './MessageInput';
13
-
14
- export interface ChatSidebarProps {
15
- /** Width of resize handle in pixels (default: 12) */
16
- resizeHandleWidth?: number;
17
- /** Show grip icon in resize handle (default: true) */
18
- showResizeIcon?: boolean;
19
- /** Custom class for resize handle */
20
- resizeHandleClassName?: string;
21
- }
22
-
23
- export const ChatSidebar = React.memo<ChatSidebarProps>(({
24
- resizeHandleWidth = 12,
25
- showResizeIcon = true,
26
- resizeHandleClassName,
27
- }) => {
28
- const {
29
- messages,
30
- isLoading,
31
- config,
32
- sendMessage,
33
- closeChat,
34
- setDisplayMode,
35
- stopStreaming,
36
- clearMessages,
37
- } = useAIChatContext();
38
- const t = useTypedT<I18nTranslations>();
39
-
40
- const labels = useMemo(() => ({
41
- aiAssistant: t('layouts.chat.aiAssistant'),
42
- defaultTitle: t('layouts.chat.defaultTitle'),
43
- newChat: t('layouts.chat.newChat'),
44
- closeChat: t('layouts.chat.closeChat'),
45
- switchToFloating: t('layouts.chat.switchToFloating'),
46
- dragToResize: t('layouts.chat.dragToResize'),
47
- howCanIHelp: t('layouts.chat.howCanIHelp'),
48
- }), [t]);
49
-
50
- // Use the layout hook for content pushing and resizing
51
- const { applyLayout, getSidebarStyles, startResize, isResizing } = useChatLayout();
52
-
53
- // Apply sidebar layout on mount, reset on unmount
54
- // eslint-disable-next-line react-hooks/exhaustive-deps
55
- useEffect(() => {
56
- applyLayout('sidebar');
57
- return () => {
58
- applyLayout('closed');
59
- };
60
- }, []);
61
-
62
- const sidebarStyles = getSidebarStyles();
63
-
64
- return (
65
- <div
66
- className="flex bg-background"
67
- style={sidebarStyles}
68
- data-chat-sidebar-panel
69
- >
70
- {/* Resize handle */}
71
- <div
72
- className={`
73
- flex items-center justify-center cursor-ew-resize
74
- border-l border-border transition-colors select-none flex-shrink-0
75
- ${isResizing ? 'bg-primary/20' : 'bg-muted/30 hover:bg-muted/50'}
76
- ${resizeHandleClassName || ''}
77
- `}
78
- style={{ width: resizeHandleWidth }}
79
- onMouseDown={startResize}
80
- title={labels.dragToResize}
81
- >
82
- {showResizeIcon && (
83
- <GripVertical className={`h-4 w-4 ${isResizing ? 'text-primary' : 'text-muted-foreground/50'}`} />
84
- )}
85
- </div>
86
-
87
- {/* Main sidebar content */}
88
- <div className="flex flex-col flex-1 min-w-0">
89
- {/* Header - uses CSS variable for navbar height consistency */}
90
- <div
91
- className="flex items-center justify-between px-4 border-b border-border bg-muted/30"
92
- style={{ height: 'var(--nextra-navbar-height, 64px)', minHeight: 'var(--nextra-navbar-height, 64px)' }}
93
- >
94
- <div className="flex items-center gap-2">
95
- <div
96
- className="rounded-full bg-primary/10 flex items-center justify-center"
97
- style={{ width: '32px', height: '32px' }}
98
- >
99
- <Bot className="h-4 w-4 text-primary" />
100
- </div>
101
- <div>
102
- <h3 className="font-semibold text-sm">{config.title || labels.defaultTitle}</h3>
103
- <p className="text-xs text-muted-foreground">{labels.aiAssistant}</p>
104
- </div>
105
- </div>
106
- <div className="flex gap-1">
107
- {messages.length > 0 && (
108
- <Button
109
- variant="ghost"
110
- size="icon"
111
- className="h-8 w-8"
112
- onClick={clearMessages}
113
- title={labels.newChat}
114
- >
115
- <RotateCcw className="h-4 w-4" />
116
- </Button>
117
- )}
118
- <Button
119
- variant="ghost"
120
- size="icon"
121
- className="h-8 w-8"
122
- onClick={() => setDisplayMode('floating')}
123
- title={labels.switchToFloating}
124
- >
125
- <PanelRightClose className="h-4 w-4" />
126
- </Button>
127
- <Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title={labels.closeChat}>
128
- <X className="h-4 w-4" />
129
- </Button>
130
- </div>
131
- </div>
132
-
133
- {/* Messages */}
134
- <div className="flex-1 overflow-hidden">
135
- <ChatMessages
136
- messages={messages}
137
- isLoading={isLoading}
138
- greeting={config.greeting}
139
- onStopStreaming={stopStreaming}
140
- isCompact={false}
141
- largeGreetingIcon
142
- greetingIcon="message"
143
- greetingTitle={labels.howCanIHelp}
144
- />
145
- </div>
146
-
147
- {/* Input */}
148
- <div className="p-4 border-t border-border bg-muted/30">
149
- <AIMessageInput onSend={sendMessage} isLoading={isLoading} placeholder={config.placeholder} />
150
- </div>
151
- </div>
152
- </div>
153
- );
154
- });
155
-
156
- ChatSidebar.displayName = 'ChatSidebar';
@@ -1,115 +0,0 @@
1
- 'use client';
2
-
3
- import { MessageCircle } from 'lucide-react';
4
- import React from 'react';
5
-
6
- import { Button, Portal } from '@djangocfg/ui-core';
7
-
8
- import { ChatProvider, useChatContext, useChatContextOptional } from '../context';
9
- import { useChatLayout } from '../hooks/useChatLayout';
10
- import { ChatPanel } from './ChatPanel';
11
- import { ChatSidebar } from './ChatSidebar';
12
-
13
- import type { ChatWidgetConfig } from '../types';
14
-
15
- export interface ChatWidgetProps extends ChatWidgetConfig {
16
- /** Custom class name for the container */
17
- className?: string;
18
- }
19
-
20
- /**
21
- * Internal chat widget that uses context
22
- */
23
- const ChatWidgetInternal: React.FC<{ className?: string }> = ({ className }) => {
24
- const { config, displayMode, openChat, isMobile } = useChatContext();
25
-
26
- // Use layout hook for consistent positioning
27
- const { getFabStyles, getFloatingStyles } = useChatLayout();
28
-
29
- const position = config.position || 'bottom-right';
30
- const fabStyles = getFabStyles(position);
31
- const floatingStyles = getFloatingStyles(position);
32
-
33
- // Mobile fullscreen styles for floating mode
34
- // Using inset: 0 with height: 100dvh for proper mobile viewport handling
35
- const mobileFullscreenStyles: React.CSSProperties = {
36
- position: 'fixed',
37
- inset: 0,
38
- height: '100dvh',
39
- width: '100vw',
40
- zIndex: 400, // Higher z-index for mobile overlay
41
- overflow: 'hidden',
42
- };
43
-
44
- // Mode: closed - just show FAB
45
- if (displayMode === 'closed') {
46
- return (
47
- <Portal>
48
- <div style={fabStyles} className={className || ''}>
49
- <Button
50
- onClick={openChat}
51
- className="rounded-full shadow-lg hover:shadow-xl transition-shadow"
52
- style={{ width: '56px', height: '56px' }}
53
- >
54
- <MessageCircle className="h-6 w-6" />
55
- </Button>
56
- </div>
57
- </Portal>
58
- );
59
- }
60
-
61
- // Mode: sidebar - full-height panel on the right (desktop only)
62
- if (displayMode === 'sidebar') {
63
- return (
64
- <Portal>
65
- <ChatSidebar />
66
- </Portal>
67
- );
68
- }
69
-
70
- // Mode: floating - fullscreen on mobile, floating panel on desktop
71
- return (
72
- <Portal>
73
- <div style={isMobile ? mobileFullscreenStyles : floatingStyles} className={className || ''}>
74
- <ChatPanel />
75
- </div>
76
- </Portal>
77
- );
78
- };
79
-
80
- /**
81
- * ChatWidget component
82
- *
83
- * Can be used in two ways:
84
- * 1. Standalone (wraps itself in ChatProvider)
85
- * 2. Inside a ChatProvider (uses context directly)
86
- */
87
- export const ChatWidget: React.FC<ChatWidgetProps> = ({
88
- apiEndpoint = '/api/chat',
89
- title = 'DjangoCFG AI',
90
- placeholder = 'Ask about DjangoCFG...',
91
- greeting = "Hi! I'm your DjangoCFG documentation assistant. Ask me anything about configuration, features, or how to use the library.",
92
- position = 'bottom-right',
93
- variant = 'default',
94
- className,
95
- }) => {
96
- // Check if we're inside a ChatProvider
97
- const existingContext = useChatContextOptional();
98
-
99
- // If already in context, use internal widget directly
100
- if (existingContext) {
101
- return <ChatWidgetInternal className={className} />;
102
- }
103
-
104
- // Otherwise, wrap in provider
105
- return (
106
- <ChatProvider
107
- apiEndpoint={apiEndpoint}
108
- config={{ title, placeholder, greeting, position, variant }}
109
- >
110
- <ChatWidgetInternal className={className} />
111
- </ChatProvider>
112
- );
113
- };
114
-
115
- ChatWidget.displayName = 'ChatWidget';
@@ -1,142 +0,0 @@
1
- 'use client';
2
-
3
- import { Bot, ExternalLink, Loader2, User } from 'lucide-react';
4
- import React from 'react';
5
-
6
- import { useAuth } from '@djangocfg/api/auth';
7
- import {
8
- Avatar, AvatarFallback, AvatarImage, Badge, Card, CardContent
9
- } from '@djangocfg/ui-core';
10
- import { MarkdownMessage } from '@djangocfg/ui-tools';
11
-
12
- import type { AIChatMessage } from '../types';
13
-
14
- export interface MessageBubbleProps {
15
- message: AIChatMessage;
16
- isCompact?: boolean;
17
- }
18
-
19
- function formatTime(date: Date): string {
20
- return date.toLocaleTimeString('en-US', {
21
- hour: '2-digit',
22
- minute: '2-digit',
23
- });
24
- }
25
-
26
- export const MessageBubble = React.memo<MessageBubbleProps>(
27
- ({ message, isCompact = false }) => {
28
- const isUser = message.role === 'user';
29
- const isAssistant = message.role === 'assistant';
30
- const { user, isAuthenticated } = useAuth();
31
-
32
- // Prepare user data BEFORE render
33
- const showUserAvatar = isUser && isAuthenticated && user;
34
- const userAvatar = user?.avatar || '';
35
- const userDisplayName = user?.display_username || user?.email || 'User';
36
- const userInitial = userDisplayName.charAt(0).toUpperCase();
37
- const avatarSize = isCompact ? '28px' : '36px';
38
- const iconSize = isCompact ? 'h-3.5 w-3.5' : 'h-4 w-4';
39
-
40
- return (
41
- <div
42
- className={`flex gap-3 animate-in fade-in slide-in-from-bottom-2 duration-300 max-w-full overflow-hidden ${
43
- isUser ? 'flex-row-reverse' : ''
44
- }`}
45
- >
46
- {/* Avatar */}
47
- {showUserAvatar ? (
48
- // Authenticated user avatar
49
- <Avatar className="flex-shrink-0" style={{ width: avatarSize, height: avatarSize }}>
50
- <AvatarImage src={userAvatar} alt={userDisplayName} />
51
- <AvatarFallback className="bg-primary text-primary-foreground text-xs">
52
- {userInitial}
53
- </AvatarFallback>
54
- </Avatar>
55
- ) : isUser ? (
56
- // Guest user icon
57
- <div
58
- className="flex-shrink-0 rounded-full flex items-center justify-center bg-primary text-primary-foreground"
59
- style={{ width: avatarSize, height: avatarSize }}
60
- >
61
- <User className={iconSize} />
62
- </div>
63
- ) : (
64
- // Bot icon
65
- <div
66
- className="flex-shrink-0 rounded-full flex items-center justify-center bg-muted text-muted-foreground"
67
- style={{ width: avatarSize, height: avatarSize }}
68
- >
69
- <Bot className={iconSize} />
70
- </div>
71
- )}
72
-
73
- {/* Message Content */}
74
- <div className={`flex-1 min-w-0 ${isUser ? 'max-w-[80%] ml-auto' : 'max-w-[85%]'}`}>
75
- {/* Header */}
76
- <div className={`flex items-baseline gap-2 mb-1 ${isUser ? 'justify-end' : ''}`}>
77
- <span className={`font-medium ${isCompact ? 'text-xs' : 'text-sm'}`}>
78
- {isUser ? userDisplayName : 'DjangoCFG AI'}
79
- </span>
80
- <span className="text-xs text-muted-foreground">
81
- {formatTime(message.timestamp)}
82
- </span>
83
- </div>
84
-
85
- {/* Message Bubble */}
86
- <Card
87
- className={`transition-all duration-200 ${
88
- isUser
89
- ? 'bg-primary text-primary-foreground ml-auto'
90
- : 'bg-muted'
91
- }`}
92
- >
93
- <CardContent className={isCompact ? 'p-2' : 'p-3'}>
94
- {/* Message Text */}
95
- <div className={`${isCompact ? 'text-xs' : 'text-sm'} overflow-hidden`} style={{ overflowWrap: 'anywhere', wordBreak: 'break-word' }}>
96
- {/* Always render markdown for immediate code block highlighting */}
97
- <MarkdownMessage
98
- content={message.content}
99
- isUser={isUser}
100
- isCompact={isCompact}
101
- />
102
- {/* Show spinner during streaming */}
103
- {message.isStreaming && (
104
- <Loader2 className="inline-block ml-1 h-3 w-3 animate-spin" />
105
- )}
106
- </div>
107
-
108
- {/* Sources */}
109
- {isAssistant && message.sources && message.sources.length > 0 && (
110
- <div className="mt-3 pt-2 border-t border-border/50">
111
- <p className="text-xs text-muted-foreground mb-1.5">Related docs:</p>
112
- <div className="flex flex-wrap gap-1.5">
113
- {message.sources.slice(0, 3).map((source, idx) => (
114
- <a
115
- key={idx}
116
- href={source.url || source.path}
117
- target="_blank"
118
- rel="noopener noreferrer"
119
- className="inline-block"
120
- >
121
- <Badge
122
- variant="outline"
123
- className="text-xs gap-1 hover:bg-primary/10 transition-colors cursor-pointer"
124
- >
125
- {source.title}
126
- {source.section && ` - ${source.section}`}
127
- <ExternalLink className="h-2.5 w-2.5" />
128
- </Badge>
129
- </a>
130
- ))}
131
- </div>
132
- </div>
133
- )}
134
- </CardContent>
135
- </Card>
136
- </div>
137
- </div>
138
- );
139
- }
140
- );
141
-
142
- MessageBubble.displayName = 'MessageBubble';
@@ -1,140 +0,0 @@
1
- 'use client';
2
-
3
- import { Loader2, Send } from 'lucide-react';
4
- import React, { useCallback, useEffect, useRef, useState } from 'react';
5
-
6
- import { Button } from '@djangocfg/ui-core';
7
-
8
- export interface AIMessageInputProps {
9
- onSend: (message: string) => void;
10
- disabled?: boolean;
11
- isLoading?: boolean;
12
- placeholder?: string;
13
- maxRows?: number;
14
- }
15
-
16
- export const AIMessageInput = React.memo<AIMessageInputProps>(
17
- ({
18
- onSend,
19
- disabled = false,
20
- isLoading = false,
21
- placeholder = 'Ask about DjangoCFG...',
22
- maxRows = 5,
23
- }) => {
24
- const [value, setValue] = useState('');
25
- const textareaRef = useRef<HTMLTextAreaElement>(null);
26
-
27
- // Auto-resize textarea
28
- const adjustHeight = useCallback(() => {
29
- const textarea = textareaRef.current;
30
- if (!textarea) return;
31
-
32
- // Reset height to auto to get the correct scrollHeight
33
- textarea.style.height = 'auto';
34
-
35
- // Calculate line height and max height
36
- const lineHeight = 24; // ~1.5rem
37
- const minHeight = 44; // Single line height with padding
38
- const maxHeight = lineHeight * maxRows + 20; // padding
39
-
40
- // Set new height
41
- const newHeight = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);
42
- textarea.style.height = `${newHeight}px`;
43
- }, [maxRows]);
44
-
45
- // Adjust height when value changes
46
- useEffect(() => {
47
- adjustHeight();
48
- }, [value, adjustHeight]);
49
-
50
- const handleSubmit = useCallback(
51
- (e?: React.FormEvent) => {
52
- e?.preventDefault();
53
-
54
- const trimmed = value.trim();
55
- if (!trimmed || disabled || isLoading) return;
56
-
57
- onSend(trimmed);
58
- setValue('');
59
-
60
- // Reset height after sending
61
- if (textareaRef.current) {
62
- textareaRef.current.style.height = 'auto';
63
- }
64
- textareaRef.current?.focus();
65
- },
66
- [value, disabled, isLoading, onSend]
67
- );
68
-
69
- const handleKeyDown = useCallback(
70
- (e: React.KeyboardEvent) => {
71
- if (e.key === 'Enter' && !e.shiftKey) {
72
- e.preventDefault();
73
- handleSubmit();
74
- }
75
- },
76
- [handleSubmit]
77
- );
78
-
79
- const canSend = value.trim().length > 0 && !disabled && !isLoading;
80
-
81
- return (
82
- <form onSubmit={handleSubmit} className="w-full">
83
- <div
84
- className="relative flex items-end rounded-2xl border border-input bg-background transition-colors focus-within:ring-1 focus-within:ring-ring focus-within:border-ring"
85
- style={{ minHeight: '44px' }}
86
- >
87
- {/* Textarea */}
88
- <textarea
89
- ref={textareaRef}
90
- value={value}
91
- onChange={(e) => setValue(e.target.value)}
92
- onKeyDown={handleKeyDown}
93
- placeholder={placeholder}
94
- disabled={disabled || isLoading}
95
- rows={1}
96
- className="flex-1 resize-none bg-transparent px-4 py-3 text-sm placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 pr-12"
97
- style={{
98
- minHeight: '44px',
99
- maxHeight: `${24 * maxRows + 20}px`,
100
- lineHeight: '1.5rem',
101
- }}
102
- autoComplete="off"
103
- />
104
-
105
- {/* Send Button - positioned inside, bottom right */}
106
- <div
107
- className="absolute flex items-center justify-center"
108
- style={{
109
- right: '6px',
110
- bottom: '6px',
111
- }}
112
- >
113
- <Button
114
- type="submit"
115
- size="icon"
116
- disabled={!canSend}
117
- className="h-8 w-8 rounded-full transition-all"
118
- style={{
119
- opacity: canSend ? 1 : 0.5,
120
- }}
121
- >
122
- {isLoading ? (
123
- <Loader2 className="h-4 w-4 animate-spin" />
124
- ) : (
125
- <Send className="h-4 w-4" />
126
- )}
127
- </Button>
128
- </div>
129
- </div>
130
-
131
- {/* Helper text */}
132
- <p className="mt-1.5 text-xs text-muted-foreground text-center">
133
- Press Enter to send, Shift+Enter for new line
134
- </p>
135
- </form>
136
- );
137
- }
138
- );
139
-
140
- AIMessageInput.displayName = 'AIMessageInput';