@djangocfg/layouts 2.1.19 → 2.1.21

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 (31) hide show
  1. package/package.json +5 -5
  2. package/src/layouts/AdminLayout/AdminLayout.tsx +1 -1
  3. package/src/layouts/AppLayout/AppLayout.tsx +29 -27
  4. package/src/layouts/AppLayout/BaseApp.tsx +36 -38
  5. package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -1
  6. package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +1 -1
  7. package/src/layouts/PublicLayout/PublicLayout.tsx +9 -43
  8. package/src/layouts/PublicLayout/components/PublicFooter/DjangoCFGLogo.tsx +45 -0
  9. package/src/layouts/PublicLayout/components/PublicFooter/FooterBottom.tsx +114 -0
  10. package/src/layouts/PublicLayout/components/PublicFooter/FooterMenuSections.tsx +53 -0
  11. package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +77 -0
  12. package/src/layouts/PublicLayout/components/PublicFooter/FooterSocialLinks.tsx +82 -0
  13. package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +129 -0
  14. package/src/layouts/PublicLayout/components/PublicFooter/index.ts +17 -0
  15. package/src/layouts/PublicLayout/components/PublicFooter/types.ts +57 -0
  16. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +3 -6
  17. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +3 -6
  18. package/src/layouts/PublicLayout/index.ts +12 -1
  19. package/src/layouts/_components/UserMenu.tsx +161 -40
  20. package/src/layouts/index.ts +4 -1
  21. package/src/layouts/shared/README.md +86 -0
  22. package/src/layouts/shared/index.ts +21 -0
  23. package/src/layouts/shared/types.ts +215 -0
  24. package/src/snippets/McpChat/components/AIChatWidget.tsx +150 -53
  25. package/src/snippets/McpChat/components/AskAIButton.tsx +2 -5
  26. package/src/snippets/McpChat/components/ChatMessages.tsx +30 -9
  27. package/src/snippets/McpChat/components/ChatPanel.tsx +1 -1
  28. package/src/snippets/McpChat/components/ChatSidebar.tsx +1 -1
  29. package/src/snippets/McpChat/components/MessageBubble.tsx +46 -34
  30. package/src/snippets/McpChat/context/AIChatContext.tsx +23 -6
  31. package/src/layouts/PublicLayout/components/PublicFooter.tsx +0 -190
@@ -11,6 +11,7 @@ import type { AIChatMessage } from '../types';
11
11
  */
12
12
  export interface ChatMessagesHandle {
13
13
  scrollToBottom: (instant?: boolean) => void;
14
+ scrollToLastMessage: (instant?: boolean) => void;
14
15
  }
15
16
 
16
17
  export interface ChatMessagesProps {
@@ -57,6 +58,7 @@ export const ChatMessages = forwardRef<ChatMessagesHandle, ChatMessagesProps>(
57
58
  ref
58
59
  ) => {
59
60
  const scrollAreaRef = useRef<ScrollAreaHandle>(null);
61
+ const messagesContainerRef = useRef<HTMLDivElement>(null);
60
62
  const hasScrolledOnLoad = useRef(false);
61
63
 
62
64
  // Scroll to bottom helper
@@ -66,27 +68,44 @@ export const ChatMessages = forwardRef<ChatMessagesHandle, ChatMessagesProps>(
66
68
  }
67
69
  }, []);
68
70
 
69
- // Expose scrollToBottom via ref
71
+ // Scroll to last message (shows top of last message)
72
+ const scrollToLastMessage = useCallback((instant = false) => {
73
+ if (messagesContainerRef.current) {
74
+ const messageElements = messagesContainerRef.current.querySelectorAll('[data-message-bubble]');
75
+ const lastMessage = messageElements[messageElements.length - 1];
76
+
77
+ if (lastMessage) {
78
+ lastMessage.scrollIntoView({
79
+ behavior: instant ? 'instant' : 'smooth',
80
+ block: 'start',
81
+ inline: 'nearest',
82
+ });
83
+ }
84
+ }
85
+ }, []);
86
+
87
+ // Expose methods via ref
70
88
  useImperativeHandle(ref, () => ({
71
89
  scrollToBottom,
72
- }), [scrollToBottom]);
90
+ scrollToLastMessage,
91
+ }), [scrollToBottom, scrollToLastMessage]);
73
92
 
74
93
  // Initial scroll when history loads (instant, no animation)
75
94
  useEffect(() => {
76
95
  if (!isLoading && messages.length > 0 && !hasScrolledOnLoad.current) {
77
96
  requestAnimationFrame(() => {
78
- scrollToBottom(true);
97
+ scrollToLastMessage(true);
79
98
  hasScrolledOnLoad.current = true;
80
99
  });
81
100
  }
82
- }, [isLoading, messages.length, scrollToBottom]);
101
+ }, [isLoading, messages.length, scrollToLastMessage]);
83
102
 
84
- // Scroll to bottom on new messages (smooth animation)
103
+ // Scroll to last message on new messages (smooth animation, shows top of message)
85
104
  useEffect(() => {
86
105
  if (hasScrolledOnLoad.current && messages.length > 0) {
87
- scrollToBottom(false);
106
+ scrollToLastMessage(false);
88
107
  }
89
- }, [messages, scrollToBottom]);
108
+ }, [messages, scrollToLastMessage]);
90
109
 
91
110
  const GreetingIcon = greetingIcon === 'message' ? MessageSquare : Bot;
92
111
  const iconSize = largeGreetingIcon ? { container: '64px', icon: 'h-8 w-8' } : { container: '48px', icon: 'h-6 w-6' };
@@ -94,7 +113,7 @@ export const ChatMessages = forwardRef<ChatMessagesHandle, ChatMessagesProps>(
94
113
 
95
114
  return (
96
115
  <ScrollArea ref={scrollAreaRef} className="h-full w-full">
97
- <div className={`${isCompact ? 'p-3' : 'p-4'} space-y-4 max-w-full overflow-x-hidden`}>
116
+ <div ref={messagesContainerRef} className={`${isCompact ? 'p-3' : 'p-4'} space-y-4 max-w-full overflow-x-hidden`}>
98
117
  {/* Greeting */}
99
118
  {messages.length === 0 && greeting && (
100
119
  <div className={`text-center ${padding}`}>
@@ -115,7 +134,9 @@ export const ChatMessages = forwardRef<ChatMessagesHandle, ChatMessagesProps>(
115
134
 
116
135
  {/* Messages */}
117
136
  {messages.map((message) => (
118
- <MessageBubble key={message.id} message={message} isCompact={isCompact} />
137
+ <div key={message.id} data-message-bubble>
138
+ <MessageBubble message={message} isCompact={isCompact} />
139
+ </div>
119
140
  ))}
120
141
 
121
142
  {/* Loading indicator with stop button */}
@@ -59,7 +59,7 @@ export const ChatPanel = React.memo(() => {
59
59
  </div>
60
60
  <div>
61
61
  <h3 className="font-semibold text-sm">{config.title || 'DjangoCFG AI'}</h3>
62
- <p className="text-xs text-muted-foreground">documentation assistant</p>
62
+ <p className="text-xs text-muted-foreground">AI Assistant</p>
63
63
  </div>
64
64
  </div>
65
65
  <div className="flex gap-1">
@@ -86,7 +86,7 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
86
86
  </div>
87
87
  <div>
88
88
  <h3 className="font-semibold text-sm">{config.title || 'DjangoCFG AI'}</h3>
89
- <p className="text-xs text-muted-foreground">documentation assistant</p>
89
+ <p className="text-xs text-muted-foreground">AI Assistant</p>
90
90
  </div>
91
91
  </div>
92
92
  <div className="flex gap-1">
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import { Card, CardContent, Badge, MarkdownMessage } from '@djangocfg/ui-nextjs';
4
+ import { Card, CardContent, Badge, MarkdownMessage, Avatar, AvatarImage, AvatarFallback } from '@djangocfg/ui-nextjs';
5
5
  import { User, Bot, ExternalLink, Loader2 } from 'lucide-react';
6
+ import { useAuth } from '@djangocfg/api/auth';
6
7
  import type { AIChatMessage } from '../types';
7
8
 
8
9
  export interface MessageBubbleProps {
@@ -21,6 +22,15 @@ export const MessageBubble = React.memo<MessageBubbleProps>(
21
22
  ({ message, isCompact = false }) => {
22
23
  const isUser = message.role === 'user';
23
24
  const isAssistant = message.role === 'assistant';
25
+ const { user, isAuthenticated } = useAuth();
26
+
27
+ // Prepare user data BEFORE render
28
+ const showUserAvatar = isUser && isAuthenticated && user;
29
+ const userAvatar = user?.avatar || '';
30
+ const userDisplayName = user?.display_username || user?.email || 'User';
31
+ const userInitial = userDisplayName.charAt(0).toUpperCase();
32
+ const avatarSize = isCompact ? '28px' : '36px';
33
+ const iconSize = isCompact ? 'h-3.5 w-3.5' : 'h-4 w-4';
24
34
 
25
35
  return (
26
36
  <div
@@ -29,30 +39,38 @@ export const MessageBubble = React.memo<MessageBubbleProps>(
29
39
  }`}
30
40
  >
31
41
  {/* Avatar */}
32
- <div
33
- className={`flex-shrink-0 rounded-full flex items-center justify-center ${
34
- isUser
35
- ? 'bg-primary text-primary-foreground'
36
- : 'bg-muted text-muted-foreground'
37
- }`}
38
- style={{
39
- width: isCompact ? '28px' : '36px',
40
- height: isCompact ? '28px' : '36px',
41
- }}
42
- >
43
- {isUser ? (
44
- <User className={isCompact ? 'h-3.5 w-3.5' : 'h-4 w-4'} />
45
- ) : (
46
- <Bot className={isCompact ? 'h-3.5 w-3.5' : 'h-4 w-4'} />
47
- )}
48
- </div>
42
+ {showUserAvatar ? (
43
+ // Authenticated user avatar
44
+ <Avatar className="flex-shrink-0" style={{ width: avatarSize, height: avatarSize }}>
45
+ <AvatarImage src={userAvatar} alt={userDisplayName} />
46
+ <AvatarFallback className="bg-primary text-primary-foreground text-xs">
47
+ {userInitial}
48
+ </AvatarFallback>
49
+ </Avatar>
50
+ ) : isUser ? (
51
+ // Guest user icon
52
+ <div
53
+ className="flex-shrink-0 rounded-full flex items-center justify-center bg-primary text-primary-foreground"
54
+ style={{ width: avatarSize, height: avatarSize }}
55
+ >
56
+ <User className={iconSize} />
57
+ </div>
58
+ ) : (
59
+ // Bot icon
60
+ <div
61
+ className="flex-shrink-0 rounded-full flex items-center justify-center bg-muted text-muted-foreground"
62
+ style={{ width: avatarSize, height: avatarSize }}
63
+ >
64
+ <Bot className={iconSize} />
65
+ </div>
66
+ )}
49
67
 
50
68
  {/* Message Content */}
51
69
  <div className={`flex-1 min-w-0 ${isUser ? 'max-w-[80%] ml-auto' : 'max-w-[85%]'}`}>
52
70
  {/* Header */}
53
71
  <div className={`flex items-baseline gap-2 mb-1 ${isUser ? 'justify-end' : ''}`}>
54
72
  <span className={`font-medium ${isCompact ? 'text-xs' : 'text-sm'}`}>
55
- {isUser ? 'You' : 'DjangoCFG AI'}
73
+ {isUser ? userDisplayName : 'DjangoCFG AI'}
56
74
  </span>
57
75
  <span className="text-xs text-muted-foreground">
58
76
  {formatTime(message.timestamp)}
@@ -70,21 +88,15 @@ export const MessageBubble = React.memo<MessageBubbleProps>(
70
88
  <CardContent className={isCompact ? 'p-2' : 'p-3'}>
71
89
  {/* Message Text */}
72
90
  <div className={`${isCompact ? 'text-xs' : 'text-sm'} overflow-hidden`} style={{ overflowWrap: 'anywhere', wordBreak: 'break-word' }}>
73
- {isUser ? (
74
- <span className="whitespace-pre-wrap" style={{ overflowWrap: 'anywhere' }}>{message.content}</span>
75
- ) : message.isStreaming ? (
76
- // During streaming - show plain text to avoid parsing errors
77
- <span className="whitespace-pre-wrap" style={{ overflowWrap: 'anywhere' }}>
78
- {message.content}
79
- <Loader2 className="inline-block ml-1 h-3 w-3 animate-spin" />
80
- </span>
81
- ) : (
82
- // Streaming complete - render with markdown formatting
83
- <MarkdownMessage
84
- content={message.content}
85
- isUser={isUser}
86
- isCompact={isCompact}
87
- />
91
+ {/* Always render markdown for immediate code block highlighting */}
92
+ <MarkdownMessage
93
+ content={message.content}
94
+ isUser={isUser}
95
+ isCompact={isCompact}
96
+ />
97
+ {/* Show spinner during streaming */}
98
+ {message.isStreaming && (
99
+ <Loader2 className="inline-block ml-1 h-3 w-3 animate-spin" />
88
100
  )}
89
101
  </div>
90
102
 
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { createContext, useContext, useState, useCallback, useMemo, useEffect, type ReactNode } from 'react';
3
+ import { createContext, useContext, useState, useCallback, useMemo, useEffect, useRef, type ReactNode } from 'react';
4
4
  import { useLocalStorage, useIsMobile } from '@djangocfg/ui-nextjs/hooks';
5
5
  import { useAIChat } from '../hooks/useAIChat';
6
6
  import { mcpEndpoints, type AIChatMessage, type ChatWidgetConfig, type ChatDisplayMode } from '../types';
@@ -119,10 +119,24 @@ export function AIChatProvider({
119
119
  // Derived state: isOpen is true when not in 'closed' mode
120
120
  const isOpen = displayMode !== 'closed';
121
121
 
122
+ // Track isOpen in a ref for event handler
123
+ const isOpenRef = useRef(isOpen);
124
+ useEffect(() => {
125
+ isOpenRef.current = isOpen;
126
+ }, [isOpen]);
127
+
128
+ // Track last active mode (non-closed) to restore when reopening
129
+ const lastActiveModeRef = useRef<Exclude<ChatDisplayMode, 'closed'>>('floating');
130
+ useEffect(() => {
131
+ if (displayMode !== 'closed') {
132
+ lastActiveModeRef.current = displayMode;
133
+ }
134
+ }, [displayMode]);
135
+
122
136
  const config: ChatWidgetConfig = useMemo(
123
137
  () => ({
124
138
  apiEndpoint,
125
- title: 'DjangoCFG AI Assistant',
139
+ title: 'DjangoCFG AI',
126
140
  placeholder: 'Ask about DjangoCFG...',
127
141
  greeting:
128
142
  "Hi! I'm your DjangoCFG AI assistant powered by GPT. Ask me anything about configuration, features, or how to use the library.",
@@ -145,7 +159,8 @@ export function AIChatProvider({
145
159
  }, [clearAIMessages]);
146
160
 
147
161
  const openChat = useCallback(() => {
148
- setStoredMode('floating');
162
+ // Restore last active mode instead of always opening in floating
163
+ setStoredMode(lastActiveModeRef.current);
149
164
  setIsMinimized(false);
150
165
  }, [setStoredMode]);
151
166
 
@@ -258,12 +273,14 @@ export function AIChatProvider({
258
273
  }
259
274
  }
260
275
 
261
- // Open chat in requested mode (or default to floating)
276
+ // Open chat in requested mode, or keep current mode if already open
262
277
  if (requestedMode) {
263
278
  setDisplayMode(requestedMode);
264
- } else {
279
+ } else if (!isOpenRef.current) {
280
+ // Only open if chat is currently closed
265
281
  openChat();
266
282
  }
283
+ // If already open, keep current mode
267
284
 
268
285
  // Auto-send message if requested
269
286
  if (autoSend) {
@@ -282,7 +299,7 @@ export function AIChatProvider({
282
299
  (window as any).__MCP_CHAT_AVAILABLE__ = false;
283
300
  }
284
301
  };
285
- }, [sendMessage, openChat, setDisplayMode]);
302
+ }, [sendMessage, setDisplayMode, openChat]);
286
303
 
287
304
  return <AIChatContext.Provider value={value}>{children}</AIChatContext.Provider>;
288
305
  }
@@ -1,190 +0,0 @@
1
- /**
2
- * Public Layout Footer
3
- *
4
- * Footer component for PublicLayout
5
- */
6
-
7
- 'use client';
8
-
9
- import React from 'react';
10
- import Link from 'next/link';
11
- import { useIsMobile } from '@djangocfg/ui-nextjs/hooks';
12
-
13
- interface FooterConfig {
14
- links?: {
15
- privacy?: string;
16
- terms?: string;
17
- security?: string;
18
- cookies?: string;
19
- docs?: string;
20
- };
21
- copyright?: string;
22
- }
23
-
24
- interface PublicFooterProps {
25
- logo?: string;
26
- siteName: string;
27
- footer?: FooterConfig;
28
- }
29
-
30
- export function PublicFooter({
31
- logo,
32
- siteName,
33
- footer,
34
- }: PublicFooterProps) {
35
- const isMobile = useIsMobile();
36
- const currentYear = new Date().getFullYear();
37
- const copyright =
38
- footer?.copyright ||
39
- `© ${currentYear} ${siteName}. All rights reserved.`;
40
-
41
- if (isMobile) {
42
- return (
43
- <footer className="lg:hidden bg-background border-t border-border mt-auto">
44
- <div className="w-full px-4 py-8">
45
- {/* Project Info */}
46
- <div className="text-center space-y-4 mb-6">
47
- <div className="flex items-center justify-center gap-2">
48
- {logo && (
49
- <div className="w-6 h-6 flex items-center justify-center">
50
- <img
51
- src={logo}
52
- alt={`${siteName} Logo`}
53
- className="w-full h-full object-contain"
54
- />
55
- </div>
56
- )}
57
- <span className="text-lg font-bold text-foreground">
58
- {siteName}
59
- </span>
60
- </div>
61
- </div>
62
-
63
- {/* Quick Links */}
64
- <div className="flex flex-wrap justify-center gap-4 mb-6 items-center">
65
- {footer?.links?.docs && (
66
- <a
67
- href={footer.links.docs}
68
- target="_blank"
69
- rel="noopener noreferrer"
70
- className="text-sm text-muted-foreground hover:text-primary transition-colors"
71
- title="Documentation"
72
- >
73
- Docs
74
- </a>
75
- )}
76
- {footer?.links?.privacy && (
77
- <Link
78
- href={footer.links.privacy}
79
- className="text-sm text-muted-foreground hover:text-primary transition-colors"
80
- >
81
- Privacy
82
- </Link>
83
- )}
84
- {footer?.links?.terms && (
85
- <Link
86
- href={footer.links.terms}
87
- className="text-sm text-muted-foreground hover:text-primary transition-colors"
88
- >
89
- Terms
90
- </Link>
91
- )}
92
- </div>
93
-
94
- {/* Bottom Section */}
95
- <div className="border-t border-border pt-4">
96
- <div className="text-center space-y-2">
97
- <div className="text-xs text-muted-foreground">{copyright}</div>
98
- </div>
99
- </div>
100
- </div>
101
- </footer>
102
- );
103
- }
104
-
105
- // Desktop Footer
106
- return (
107
- <footer className="max-lg:hidden bg-background border-t border-border mt-auto">
108
- <div className="w-full px-8 lg:px-16 xl:px-24 py-12">
109
- <div className="flex flex-col gap-8">
110
- {/* Top Section */}
111
- <div className="flex gap-8">
112
- {/* Left Column - Project Info */}
113
- <div className="space-y-4" style={{ width: '30%', minWidth: '300px' }}>
114
- <div className="flex items-center gap-2">
115
- {logo && (
116
- <div className="w-8 h-8 flex items-center justify-center">
117
- <img
118
- src={logo}
119
- alt={`${siteName} Logo`}
120
- className="w-full h-full object-contain"
121
- />
122
- </div>
123
- )}
124
- <span className="text-xl font-bold text-foreground">
125
- {siteName}
126
- </span>
127
- </div>
128
- </div>
129
- </div>
130
-
131
- {/* Bottom Section */}
132
- <div
133
- className="border-t border-border"
134
- style={{ marginTop: '2rem', paddingTop: '2rem' }}
135
- >
136
- <div className="flex justify-between items-center gap-4">
137
- <div className="text-xs text-muted-foreground">{copyright}</div>
138
- <div className="flex flex-wrap items-center gap-4">
139
- {footer?.links?.docs && (
140
- <a
141
- href={footer.links.docs}
142
- target="_blank"
143
- rel="noopener noreferrer"
144
- className="text-xs text-muted-foreground hover:text-primary transition-colors"
145
- title="Documentation"
146
- >
147
- Docs
148
- </a>
149
- )}
150
- {footer?.links?.privacy && (
151
- <Link
152
- href={footer.links.privacy}
153
- className="text-xs text-muted-foreground hover:text-primary transition-colors"
154
- >
155
- Privacy Policy
156
- </Link>
157
- )}
158
- {footer?.links?.terms && (
159
- <Link
160
- href={footer.links.terms}
161
- className="text-xs text-muted-foreground hover:text-primary transition-colors"
162
- >
163
- Terms of Service
164
- </Link>
165
- )}
166
- {footer?.links?.security && (
167
- <Link
168
- href={footer.links.security}
169
- className="text-xs text-muted-foreground hover:text-primary transition-colors"
170
- >
171
- Security
172
- </Link>
173
- )}
174
- {footer?.links?.cookies && (
175
- <Link
176
- href={footer.links.cookies}
177
- className="text-xs text-muted-foreground hover:text-primary transition-colors"
178
- >
179
- Cookies
180
- </Link>
181
- )}
182
- </div>
183
- </div>
184
- </div>
185
- </div>
186
- </div>
187
- </footer>
188
- );
189
- }
190
-