@djangocfg/layouts 2.0.8 → 2.0.10

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.
@@ -2,125 +2,99 @@
2
2
 
3
3
  import React from 'react';
4
4
  import { Card, CardHeader, CardContent, CardFooter, Button } from '@djangocfg/ui';
5
- import { X, Minimize2, PanelRight, Bot } from 'lucide-react';
6
- import type { ChatDisplayMode, AIChatMessage } from '../types';
5
+ import { X, PanelRight, Bot, RotateCcw } from 'lucide-react';
7
6
  import { ChatMessages } from './ChatMessages';
8
7
  import { AIMessageInput } from './MessageInput';
8
+ import { useAIChatContext } from '../context/AIChatContext';
9
9
 
10
- export interface ChatPanelProps {
11
- messages: AIChatMessage[];
12
- isLoading: boolean;
13
- onSendMessage: (content: string) => void;
14
- onClose?: () => void;
15
- onMinimize?: () => void;
16
- onModeChange?: (mode: ChatDisplayMode) => void;
17
- onStopStreaming?: () => void;
18
- isMinimized?: boolean;
19
- isMobile?: boolean;
20
- title?: string;
21
- placeholder?: string;
22
- greeting?: string;
23
- }
24
-
25
- export const ChatPanel = React.memo<ChatPanelProps>(
26
- ({
10
+ export const ChatPanel = React.memo(() => {
11
+ const {
27
12
  messages,
28
13
  isLoading,
29
- onSendMessage,
30
- onClose,
31
- onMinimize,
32
- onModeChange,
33
- onStopStreaming,
34
- isMinimized = false,
35
- isMobile = false,
36
- title = 'DjangoCFG Assistant',
37
- placeholder,
38
- greeting,
39
- }) => {
40
- if (isMinimized) {
41
- return (
42
- <Button
43
- onClick={onMinimize}
44
- className="rounded-full shadow-lg"
45
- style={{ width: '56px', height: '56px' }}
46
- >
47
- <Bot className="h-6 w-6" />
48
- </Button>
49
- );
50
- }
14
+ config,
15
+ isMobile,
16
+ sendMessage,
17
+ closeChat,
18
+ setDisplayMode,
19
+ stopStreaming,
20
+ clearMessages,
21
+ } = useAIChatContext();
51
22
 
52
- return (
53
- <Card
54
- className="flex flex-col shadow-2xl border-border/50"
55
- style={{
56
- width: '380px',
57
- height: '520px',
58
- maxHeight: 'calc(100vh - 100px)',
59
- }}
60
- >
61
- {/* Header */}
62
- <CardHeader className="flex flex-row items-center justify-between p-3 border-b">
63
- <div className="flex items-center gap-2">
64
- <div
65
- className="rounded-full bg-primary/10 flex items-center justify-center"
66
- style={{ width: '32px', height: '32px' }}
67
- >
68
- <Bot className="h-4 w-4 text-primary" />
69
- </div>
70
- <div>
71
- <h3 className="font-semibold text-sm">{title}</h3>
72
- <p className="text-xs text-muted-foreground">AI-powered documentation assistant</p>
73
- </div>
23
+ return (
24
+ <Card
25
+ className="flex flex-col shadow-2xl border-border/50"
26
+ style={{
27
+ width: '380px',
28
+ height: '520px',
29
+ maxHeight: 'calc(100vh - 100px)',
30
+ }}
31
+ >
32
+ {/* Header */}
33
+ <CardHeader className="flex flex-row items-center justify-between p-3 border-b">
34
+ <div className="flex items-center gap-2">
35
+ <div
36
+ className="rounded-full bg-primary/10 flex items-center justify-center"
37
+ style={{ width: '32px', height: '32px' }}
38
+ >
39
+ <Bot className="h-4 w-4 text-primary" />
74
40
  </div>
75
- <div className="flex gap-1">
76
- {/* Sidebar mode button - only on desktop */}
77
- {onModeChange && !isMobile && (
78
- <Button
79
- variant="ghost"
80
- size="icon"
81
- className="h-8 w-8"
82
- onClick={() => onModeChange('sidebar')}
83
- title="Switch to sidebar mode"
84
- >
85
- <PanelRight className="h-4 w-4" />
86
- </Button>
87
- )}
88
- {onMinimize && (
89
- <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onMinimize} title="Minimize">
90
- <Minimize2 className="h-4 w-4" />
91
- </Button>
92
- )}
93
- {onClose && (
94
- <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onClose} title="Close">
95
- <X className="h-4 w-4" />
96
- </Button>
97
- )}
41
+ <div>
42
+ <h3 className="font-semibold text-sm">{config.title || 'DjangoCFG AI'}</h3>
43
+ <p className="text-xs text-muted-foreground">documentation assistant</p>
98
44
  </div>
99
- </CardHeader>
45
+ </div>
46
+ <div className="flex gap-1">
47
+ {messages.length > 0 && (
48
+ <Button
49
+ variant="ghost"
50
+ size="icon"
51
+ className="h-8 w-8"
52
+ onClick={clearMessages}
53
+ title="New chat"
54
+ >
55
+ <RotateCcw className="h-4 w-4" />
56
+ </Button>
57
+ )}
58
+ {/* Sidebar mode button - only on desktop */}
59
+ {!isMobile && (
60
+ <Button
61
+ variant="ghost"
62
+ size="icon"
63
+ className="h-8 w-8"
64
+ onClick={() => setDisplayMode('sidebar')}
65
+ title="Switch to sidebar mode"
66
+ >
67
+ <PanelRight className="h-4 w-4" />
68
+ </Button>
69
+ )}
70
+ <Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title="Close">
71
+ <X className="h-4 w-4" />
72
+ </Button>
73
+ </div>
74
+ </CardHeader>
100
75
 
101
- {/* Messages */}
102
- <CardContent className="flex-1 p-0 overflow-hidden">
103
- <ChatMessages
104
- messages={messages}
105
- isLoading={isLoading}
106
- greeting={greeting}
107
- onStopStreaming={onStopStreaming}
108
- isCompact
109
- greetingIcon="bot"
110
- />
111
- </CardContent>
76
+ {/* Messages */}
77
+ <CardContent className="flex-1 p-0 overflow-hidden">
78
+ <ChatMessages
79
+ messages={messages}
80
+ isLoading={isLoading}
81
+ greeting={config.greeting}
82
+ onStopStreaming={stopStreaming}
83
+ isCompact
84
+ greetingIcon="bot"
85
+ />
86
+ </CardContent>
112
87
 
113
- {/* Input */}
114
- <CardFooter className="p-3 border-t">
115
- <AIMessageInput
116
- onSend={onSendMessage}
117
- isLoading={isLoading}
118
- placeholder={placeholder}
119
- />
120
- </CardFooter>
121
- </Card>
122
- );
123
- }
124
- );
88
+ {/* Input */}
89
+ <CardFooter className="p-3 border-t">
90
+ <AIMessageInput
91
+ onSend={sendMessage}
92
+ isLoading={isLoading}
93
+ placeholder={config.placeholder}
94
+ />
95
+ </CardFooter>
96
+ </Card>
97
+ );
98
+ });
125
99
 
126
100
  ChatPanel.displayName = 'ChatPanel';
@@ -2,60 +2,59 @@
2
2
 
3
3
  import React, { useEffect } from 'react';
4
4
  import { Button } from '@djangocfg/ui';
5
- import { X, PanelRightClose, Bot } from 'lucide-react';
5
+ import { X, PanelRightClose, Bot, GripVertical, RotateCcw } from 'lucide-react';
6
6
  import { ChatMessages } from './ChatMessages';
7
7
  import { AIMessageInput } from './MessageInput';
8
8
  import { useChatLayout } from '../hooks/useChatLayout';
9
- import type { AIChatMessage, ChatDisplayMode } from '../types';
9
+ import { useAIChatContext } from '../context/AIChatContext';
10
10
 
11
- const SIDEBAR_WIDTH = 400;
12
-
13
- export interface ChatSidebarProps {
14
- messages: AIChatMessage[];
15
- isLoading: boolean;
16
- onSendMessage: (content: string) => void;
17
- onClose?: () => void;
18
- onModeChange?: (mode: ChatDisplayMode) => void;
19
- onStopStreaming?: () => void;
20
- title?: string;
21
- placeholder?: string;
22
- greeting?: string;
23
- }
24
-
25
- export const ChatSidebar = React.memo<ChatSidebarProps>(
26
- ({
11
+ export const ChatSidebar = React.memo(() => {
12
+ const {
27
13
  messages,
28
14
  isLoading,
29
- onSendMessage,
30
- onClose,
31
- onModeChange,
32
- onStopStreaming,
33
- title = 'DjangoCFG Assistant',
34
- placeholder,
35
- greeting,
36
- }) => {
37
- // Use the layout hook for content pushing
38
- const { applyLayout, getSidebarStyles } = useChatLayout({
39
- sidebarWidth: SIDEBAR_WIDTH,
40
- });
15
+ config,
16
+ sendMessage,
17
+ closeChat,
18
+ setDisplayMode,
19
+ stopStreaming,
20
+ clearMessages,
21
+ } = useAIChatContext();
41
22
 
42
- // Apply sidebar layout on mount, reset on unmount
43
- // eslint-disable-next-line react-hooks/exhaustive-deps
44
- useEffect(() => {
45
- applyLayout('sidebar');
46
- return () => {
47
- applyLayout('closed');
48
- };
49
- }, []);
23
+ // Use the layout hook for content pushing and resizing
24
+ const { applyLayout, getSidebarStyles, startResize, isResizing } = useChatLayout();
50
25
 
51
- const sidebarStyles = getSidebarStyles();
26
+ // Apply sidebar layout on mount, reset on unmount
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps
28
+ useEffect(() => {
29
+ applyLayout('sidebar');
30
+ return () => {
31
+ applyLayout('closed');
32
+ };
33
+ }, []);
52
34
 
53
- return (
35
+ const sidebarStyles = getSidebarStyles();
36
+
37
+ return (
38
+ <div
39
+ className="flex bg-background"
40
+ style={sidebarStyles}
41
+ data-chat-sidebar-panel
42
+ >
43
+ {/* Resize handle */}
54
44
  <div
55
- className="flex flex-col bg-background border-l border-border"
56
- style={sidebarStyles}
57
- data-chat-sidebar-panel
45
+ className={`
46
+ flex items-center justify-center w-3 cursor-ew-resize
47
+ border-l border-border transition-colors select-none flex-shrink-0
48
+ ${isResizing ? 'bg-primary/20' : 'bg-muted/30 hover:bg-muted/50'}
49
+ `}
50
+ onMouseDown={startResize}
51
+ title="Drag to resize"
58
52
  >
53
+ <GripVertical className={`h-4 w-4 ${isResizing ? 'text-primary' : 'text-muted-foreground/50'}`} />
54
+ </div>
55
+
56
+ {/* Main sidebar content */}
57
+ <div className="flex flex-col flex-1 min-w-0">
59
58
  {/* Header - uses CSS variable for navbar height consistency */}
60
59
  <div
61
60
  className="flex items-center justify-between px-4 border-b border-border bg-muted/30"
@@ -69,27 +68,34 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(
69
68
  <Bot className="h-4 w-4 text-primary" />
70
69
  </div>
71
70
  <div>
72
- <h3 className="font-semibold text-sm">{title}</h3>
73
- <p className="text-xs text-muted-foreground">Documentation assistant</p>
71
+ <h3 className="font-semibold text-sm">{config.title || 'DjangoCFG AI'}</h3>
72
+ <p className="text-xs text-muted-foreground">documentation assistant</p>
74
73
  </div>
75
74
  </div>
76
75
  <div className="flex gap-1">
77
- {onModeChange && (
76
+ {messages.length > 0 && (
78
77
  <Button
79
78
  variant="ghost"
80
79
  size="icon"
81
80
  className="h-8 w-8"
82
- onClick={() => onModeChange('floating')}
83
- title="Switch to floating mode"
81
+ onClick={clearMessages}
82
+ title="New chat"
84
83
  >
85
- <PanelRightClose className="h-4 w-4" />
86
- </Button>
87
- )}
88
- {onClose && (
89
- <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onClose} title="Close chat">
90
- <X className="h-4 w-4" />
84
+ <RotateCcw className="h-4 w-4" />
91
85
  </Button>
92
86
  )}
87
+ <Button
88
+ variant="ghost"
89
+ size="icon"
90
+ className="h-8 w-8"
91
+ onClick={() => setDisplayMode('floating')}
92
+ title="Switch to floating mode"
93
+ >
94
+ <PanelRightClose className="h-4 w-4" />
95
+ </Button>
96
+ <Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title="Close chat">
97
+ <X className="h-4 w-4" />
98
+ </Button>
93
99
  </div>
94
100
  </div>
95
101
 
@@ -98,8 +104,8 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(
98
104
  <ChatMessages
99
105
  messages={messages}
100
106
  isLoading={isLoading}
101
- greeting={greeting}
102
- onStopStreaming={onStopStreaming}
107
+ greeting={config.greeting}
108
+ onStopStreaming={stopStreaming}
103
109
  isCompact={false}
104
110
  largeGreetingIcon
105
111
  greetingIcon="message"
@@ -109,11 +115,11 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(
109
115
 
110
116
  {/* Input */}
111
117
  <div className="p-4 border-t border-border bg-muted/30">
112
- <AIMessageInput onSend={onSendMessage} isLoading={isLoading} placeholder={placeholder} />
118
+ <AIMessageInput onSend={sendMessage} isLoading={isLoading} placeholder={config.placeholder} />
113
119
  </div>
114
120
  </div>
115
- );
116
- }
117
- );
121
+ </div>
122
+ );
123
+ });
118
124
 
119
125
  ChatSidebar.displayName = 'ChatSidebar';
@@ -18,19 +18,7 @@ export interface ChatWidgetProps extends ChatWidgetConfig {
18
18
  * Internal chat widget that uses context
19
19
  */
20
20
  const ChatWidgetInternal: React.FC<{ className?: string }> = ({ className }) => {
21
- const {
22
- messages,
23
- isLoading,
24
- isMinimized,
25
- config,
26
- displayMode,
27
- isMobile,
28
- sendMessage,
29
- openChat,
30
- closeChat,
31
- toggleMinimize,
32
- setDisplayMode,
33
- } = useChatContext();
21
+ const { config, displayMode, openChat } = useChatContext();
34
22
 
35
23
  // Use layout hook for consistent positioning
36
24
  const { getFabStyles, getFloatingStyles } = useChatLayout();
@@ -60,16 +48,7 @@ const ChatWidgetInternal: React.FC<{ className?: string }> = ({ className }) =>
60
48
  if (displayMode === 'sidebar') {
61
49
  return (
62
50
  <Portal>
63
- <ChatSidebar
64
- messages={messages}
65
- isLoading={isLoading}
66
- onSendMessage={sendMessage}
67
- onClose={closeChat}
68
- onModeChange={setDisplayMode}
69
- title={config.title}
70
- placeholder={config.placeholder}
71
- greeting={config.greeting}
72
- />
51
+ <ChatSidebar />
73
52
  </Portal>
74
53
  );
75
54
  }
@@ -78,19 +57,7 @@ const ChatWidgetInternal: React.FC<{ className?: string }> = ({ className }) =>
78
57
  return (
79
58
  <Portal>
80
59
  <div style={floatingStyles} className={className || ''}>
81
- <ChatPanel
82
- messages={messages}
83
- isLoading={isLoading}
84
- onSendMessage={sendMessage}
85
- onClose={closeChat}
86
- onMinimize={toggleMinimize}
87
- onModeChange={setDisplayMode}
88
- isMinimized={isMinimized}
89
- isMobile={isMobile}
90
- title={config.title}
91
- placeholder={config.placeholder}
92
- greeting={config.greeting}
93
- />
60
+ <ChatPanel />
94
61
  </div>
95
62
  </Portal>
96
63
  );
@@ -105,7 +72,7 @@ const ChatWidgetInternal: React.FC<{ className?: string }> = ({ className }) =>
105
72
  */
106
73
  export const ChatWidget: React.FC<ChatWidgetProps> = ({
107
74
  apiEndpoint = '/api/chat',
108
- title = 'DjangoCFG Assistant',
75
+ title = 'DjangoCFG AI',
109
76
  placeholder = 'Ask about DjangoCFG...',
110
77
  greeting = "Hi! I'm your DjangoCFG documentation assistant. Ask me anything about configuration, features, or how to use the library.",
111
78
  position = 'bottom-right',
@@ -24,7 +24,7 @@ export const MessageBubble = React.memo<MessageBubbleProps>(
24
24
 
25
25
  return (
26
26
  <div
27
- className={`flex gap-3 animate-in fade-in slide-in-from-bottom-2 duration-300 ${
27
+ className={`flex gap-3 animate-in fade-in slide-in-from-bottom-2 duration-300 max-w-full overflow-hidden ${
28
28
  isUser ? 'flex-row-reverse' : ''
29
29
  }`}
30
30
  >
@@ -52,7 +52,7 @@ export const MessageBubble = React.memo<MessageBubbleProps>(
52
52
  {/* Header */}
53
53
  <div className={`flex items-baseline gap-2 mb-1 ${isUser ? 'justify-end' : ''}`}>
54
54
  <span className={`font-medium ${isCompact ? 'text-xs' : 'text-sm'}`}>
55
- {isUser ? 'You' : 'DjangoCFG Assistant'}
55
+ {isUser ? 'You' : 'DjangoCFG AI'}
56
56
  </span>
57
57
  <span className="text-xs text-muted-foreground">
58
58
  {formatTime(message.timestamp)}
@@ -69,12 +69,12 @@ export const MessageBubble = React.memo<MessageBubbleProps>(
69
69
  >
70
70
  <CardContent className={isCompact ? 'p-2' : 'p-3'}>
71
71
  {/* Message Text */}
72
- <div className={`${isCompact ? 'text-xs' : 'text-sm'}`}>
72
+ <div className={`${isCompact ? 'text-xs' : 'text-sm'} overflow-hidden`} style={{ overflowWrap: 'anywhere', wordBreak: 'break-word' }}>
73
73
  {isUser ? (
74
- <span className="whitespace-pre-wrap">{message.content}</span>
74
+ <span className="whitespace-pre-wrap" style={{ overflowWrap: 'anywhere' }}>{message.content}</span>
75
75
  ) : message.isStreaming ? (
76
76
  // During streaming - show plain text to avoid parsing errors
77
- <span className="whitespace-pre-wrap">
77
+ <span className="whitespace-pre-wrap" style={{ overflowWrap: 'anywhere' }}>
78
78
  {message.content}
79
79
  <Loader2 className="inline-block ml-1 h-3 w-3 animate-spin" />
80
80
  </span>
@@ -7,7 +7,6 @@ export { AIChatWidget } from './AIChatWidget';
7
7
  export type { AIChatWidgetProps } from './AIChatWidget';
8
8
 
9
9
  export { ChatPanel } from './ChatPanel';
10
- export type { ChatPanelProps } from './ChatPanel';
11
10
 
12
11
  export { ChatMessages } from './ChatMessages';
13
12
  export type { ChatMessagesProps, ChatMessagesHandle } from './ChatMessages';
@@ -19,4 +18,3 @@ export { AIMessageInput } from './MessageInput';
19
18
  export type { AIMessageInputProps } from './MessageInput';
20
19
 
21
20
  export { ChatSidebar } from './ChatSidebar';
22
- export type { ChatSidebarProps } from './ChatSidebar';
@@ -33,3 +33,45 @@ export const mcpEndpoints = {
33
33
  // Export defaults for backwards compatibility
34
34
  export const DEFAULT_CHAT_API_ENDPOINT = PROD_HOST + '/api/chat';
35
35
  export const DEFAULT_MCP_BASE_URL = PROD_HOST;
36
+
37
+ /**
38
+ * Chat sidebar layout configuration
39
+ */
40
+ export const sidebarConfig = {
41
+ /** Minimum sidebar width in pixels */
42
+ minWidth: 320,
43
+ /** Maximum sidebar width in pixels */
44
+ maxWidth: 600,
45
+ /** Default sidebar width in pixels */
46
+ defaultWidth: 400,
47
+ /** Z-index for chat elements */
48
+ zIndex: 300,
49
+ /** Animation duration in milliseconds */
50
+ animationDuration: 200,
51
+ };
52
+
53
+ /**
54
+ * Chat FAB (Floating Action Button) configuration
55
+ */
56
+ export const fabConfig = {
57
+ /** Bottom offset in pixels */
58
+ bottom: 24,
59
+ /** Right offset in pixels */
60
+ right: 24,
61
+ /** Size of FAB button in pixels */
62
+ size: 56,
63
+ };
64
+
65
+ /**
66
+ * LocalStorage keys for chat state persistence
67
+ */
68
+ export const storageKeys = {
69
+ /** Display mode (closed, floating, sidebar) */
70
+ mode: 'djangocfg-chat-mode',
71
+ /** User ID for conversation tracking */
72
+ userId: 'djangocfg-chat-user-id',
73
+ /** Chat messages history */
74
+ messages: 'djangocfg-chat-messages',
75
+ /** Sidebar width */
76
+ sidebarWidth: 'djangocfg-chat-sidebar-width',
77
+ };
@@ -4,10 +4,8 @@ import React, { createContext, useContext, useState, useCallback, useMemo, useEf
4
4
  import { useLocalStorage, useIsMobile } from '@djangocfg/ui/hooks';
5
5
  import { v4 as uuidv4 } from 'uuid';
6
6
  import type { AIChatMessage, ChatApiResponse, AIChatSource, ChatWidgetConfig, ChatDisplayMode } from '../types';
7
+ import { storageKeys } from '../config';
7
8
 
8
- const STORAGE_KEY_MODE = 'djangocfg-chat-mode';
9
- const STORAGE_KEY_USER_ID = 'djangocfg-chat-user-id';
10
- const STORAGE_KEY_MESSAGES = 'djangocfg-chat-messages';
11
9
  const MAX_STORED_MESSAGES = 50;
12
10
 
13
11
  function generateMessageId(): string {
@@ -102,13 +100,13 @@ export function ChatProvider({
102
100
  const isHydratedRef = useRef(false);
103
101
 
104
102
  // Display mode with localStorage persistence
105
- const [storedMode, setStoredMode] = useLocalStorage<ChatDisplayMode>(STORAGE_KEY_MODE, 'closed');
103
+ const [storedMode, setStoredMode] = useLocalStorage<ChatDisplayMode>(storageKeys.mode, 'closed');
106
104
 
107
105
  // User ID with localStorage persistence
108
- const [userId, setUserId] = useLocalStorage<string>(STORAGE_KEY_USER_ID, '');
106
+ const [userId, setUserId] = useLocalStorage<string>(storageKeys.userId, '');
109
107
 
110
108
  // Messages storage (serialized)
111
- const [storedMessages, setStoredMessages] = useLocalStorage<SerializedMessage[]>(STORAGE_KEY_MESSAGES, []);
109
+ const [storedMessages, setStoredMessages] = useLocalStorage<SerializedMessage[]>(storageKeys.messages, []);
112
110
 
113
111
  const isMobile = useIsMobile();
114
112
 
@@ -163,7 +161,7 @@ export function ChatProvider({
163
161
  const config: ChatWidgetConfig = useMemo(
164
162
  () => ({
165
163
  apiEndpoint,
166
- title: 'DjangoCFG Assistant',
164
+ title: 'DjangoCFG AI',
167
165
  placeholder: 'Ask about DjangoCFG...',
168
166
  greeting:
169
167
  "Hi! I'm your DjangoCFG documentation assistant. Ask me anything about configuration, features, or how to use the library.",