@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.
- package/README.md +3 -17
- package/package.json +18 -18
- package/src/components/errors/ErrorLayout.tsx +2 -2
- package/src/components/errors/ErrorsTracker/index.ts +1 -0
- package/src/components/errors/ErrorsTracker/utils/formatters.ts +23 -1
- package/src/hooks/useLogout.ts +9 -12
- package/src/layouts/AppLayout/AppLayout.tsx +20 -8
- package/src/layouts/AppLayout/BaseApp.tsx +5 -28
- package/src/layouts/AuthLayout/AuthLayout.tsx +51 -22
- package/src/layouts/AuthLayout/README.md +78 -0
- package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +2 -2
- package/src/layouts/AuthLayout/components/shared/AuthError.tsx +10 -2
- package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +2 -2
- package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +3 -2
- package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +4 -1
- package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +2 -2
- package/src/layouts/AuthLayout/components/shared/index.ts +0 -2
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +25 -80
- package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +8 -13
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +2 -2
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +2 -2
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +2 -2
- package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +61 -42
- package/src/layouts/AuthLayout/context.tsx +0 -2
- package/src/layouts/AuthLayout/index.ts +9 -6
- package/src/layouts/AuthLayout/styles/auth.css +265 -120
- package/src/layouts/AuthLayout/types.ts +60 -7
- package/src/layouts/ProfileLayout/.claude/.sidecar/activity.jsonl +2 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/history/2026-03-15.md +35 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/review.md +35 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/scan.log +3 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-001.md +18 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-002.md +19 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-003.md +18 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-004.md +18 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-005.md +18 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/usage.json +5 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +52 -403
- package/src/layouts/ProfileLayout/components/ActionButton.tsx +38 -0
- package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +109 -148
- package/src/layouts/ProfileLayout/components/EditableField.tsx +119 -0
- package/src/layouts/ProfileLayout/components/Section.tsx +22 -0
- package/src/layouts/ProfileLayout/components/index.ts +4 -1
- package/src/layouts/ProfileLayout/context.tsx +31 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +2 -2
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +2 -2
- package/src/layouts/_components/UserMenu.tsx +2 -2
- package/src/layouts/types/README.md +0 -20
- package/src/layouts/types/index.ts +2 -2
- package/src/layouts/types/layout.types.ts +2 -5
- package/src/layouts/types/providers.types.ts +0 -27
- package/src/snippets/AuthDialog/AuthDialog.tsx +2 -2
- package/src/snippets/Breadcrumbs.tsx +2 -2
- package/src/snippets/index.ts +0 -67
- package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +0 -56
- package/src/snippets/McpChat/README.md +0 -441
- package/src/snippets/McpChat/components/AIChatWidget.tsx +0 -361
- package/src/snippets/McpChat/components/AskAIButton.tsx +0 -92
- package/src/snippets/McpChat/components/ChatMessages.tsx +0 -138
- package/src/snippets/McpChat/components/ChatPanel.tsx +0 -131
- package/src/snippets/McpChat/components/ChatSidebar.tsx +0 -156
- package/src/snippets/McpChat/components/ChatWidget.tsx +0 -115
- package/src/snippets/McpChat/components/MessageBubble.tsx +0 -142
- package/src/snippets/McpChat/components/MessageInput.tsx +0 -140
- package/src/snippets/McpChat/components/index.ts +0 -24
- package/src/snippets/McpChat/config.ts +0 -94
- package/src/snippets/McpChat/context/AIChatContext.tsx +0 -327
- package/src/snippets/McpChat/context/ChatContext.tsx +0 -361
- package/src/snippets/McpChat/context/index.ts +0 -7
- package/src/snippets/McpChat/hooks/index.ts +0 -6
- package/src/snippets/McpChat/hooks/useAIChat.ts +0 -503
- package/src/snippets/McpChat/hooks/useChatLayout.ts +0 -442
- package/src/snippets/McpChat/hooks/useMcpChat.ts +0 -90
- package/src/snippets/McpChat/index.ts +0 -79
- package/src/snippets/McpChat/types.ts +0 -189
- package/src/snippets/PWAInstall/@docs/README.md +0 -92
- package/src/snippets/PWAInstall/@docs/research/ios-android-install-flows.md +0 -576
- package/src/snippets/PWAInstall/README.md +0 -235
- package/src/snippets/PWAInstall/components/A2HSHint.tsx +0 -236
- package/src/snippets/PWAInstall/components/DesktopGuide.tsx +0 -234
- package/src/snippets/PWAInstall/components/IOSGuide.tsx +0 -29
- package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +0 -103
- package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +0 -103
- package/src/snippets/PWAInstall/components/PWAPageResumeManager.tsx +0 -33
- package/src/snippets/PWAInstall/context/InstallContext.tsx +0 -102
- package/src/snippets/PWAInstall/hooks/useInstallPrompt.ts +0 -168
- package/src/snippets/PWAInstall/hooks/useIsPWA.ts +0 -116
- package/src/snippets/PWAInstall/hooks/usePWAPageResume.ts +0 -163
- package/src/snippets/PWAInstall/index.ts +0 -80
- package/src/snippets/PWAInstall/types/components.ts +0 -95
- package/src/snippets/PWAInstall/types/config.ts +0 -29
- package/src/snippets/PWAInstall/types/index.ts +0 -26
- package/src/snippets/PWAInstall/types/install.ts +0 -38
- package/src/snippets/PWAInstall/types/platform.ts +0 -29
- package/src/snippets/PWAInstall/utils/localStorage.ts +0 -181
- package/src/snippets/PWAInstall/utils/logger.ts +0 -149
- 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';
|