@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.
- package/package.json +5 -5
- package/src/auth/context/AuthContext.tsx +11 -6
- package/src/auth/hooks/useAuthGuard.ts +2 -2
- package/src/auth/hooks/useAutoAuth.ts +2 -2
- package/src/auth/hooks/useGithubAuth.ts +4 -3
- package/src/components/RedirectPage/RedirectPage.tsx +2 -2
- package/src/components/core/ClientOnly.tsx +73 -0
- package/src/components/core/index.ts +2 -0
- package/src/components/errors/ErrorLayout.tsx +6 -7
- package/src/layouts/AppLayout/AppLayout.tsx +25 -20
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +9 -21
- package/src/snippets/AuthDialog/AuthDialog.tsx +2 -2
- package/src/snippets/McpChat/components/AIChatWidget.tsx +3 -39
- package/src/snippets/McpChat/components/ChatMessages.tsx +2 -2
- package/src/snippets/McpChat/components/ChatPanel.tsx +84 -110
- package/src/snippets/McpChat/components/ChatSidebar.tsx +66 -60
- package/src/snippets/McpChat/components/ChatWidget.tsx +4 -37
- package/src/snippets/McpChat/components/MessageBubble.tsx +5 -5
- package/src/snippets/McpChat/components/index.ts +0 -2
- package/src/snippets/McpChat/config.ts +42 -0
- package/src/snippets/McpChat/context/ChatContext.tsx +5 -7
- package/src/snippets/McpChat/hooks/useChatLayout.ts +134 -23
- package/src/snippets/McpChat/index.ts +0 -1
- package/src/snippets/index.ts +0 -1
|
@@ -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,
|
|
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
|
|
11
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
9
|
+
import { useAIChatContext } from '../context/AIChatContext';
|
|
10
10
|
|
|
11
|
-
const
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
56
|
-
|
|
57
|
-
|
|
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">
|
|
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
|
-
{
|
|
76
|
+
{messages.length > 0 && (
|
|
78
77
|
<Button
|
|
79
78
|
variant="ghost"
|
|
80
79
|
size="icon"
|
|
81
80
|
className="h-8 w-8"
|
|
82
|
-
onClick={
|
|
83
|
-
title="
|
|
81
|
+
onClick={clearMessages}
|
|
82
|
+
title="New chat"
|
|
84
83
|
>
|
|
85
|
-
<
|
|
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={
|
|
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={
|
|
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
|
|
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
|
|
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>(
|
|
103
|
+
const [storedMode, setStoredMode] = useLocalStorage<ChatDisplayMode>(storageKeys.mode, 'closed');
|
|
106
104
|
|
|
107
105
|
// User ID with localStorage persistence
|
|
108
|
-
const [userId, setUserId] = useLocalStorage<string>(
|
|
106
|
+
const [userId, setUserId] = useLocalStorage<string>(storageKeys.userId, '');
|
|
109
107
|
|
|
110
108
|
// Messages storage (serialized)
|
|
111
|
-
const [storedMessages, setStoredMessages] = useLocalStorage<SerializedMessage[]>(
|
|
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
|
|
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.",
|