@djangocfg/layouts 2.0.7 → 2.0.9
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 +65 -6
- package/package.json +5 -5
- package/src/auth/context/AuthContext.tsx +11 -6
- package/src/auth/hooks/index.ts +1 -0
- package/src/auth/hooks/useAuthGuard.ts +2 -2
- package/src/auth/hooks/useAutoAuth.ts +2 -2
- package/src/auth/hooks/useGithubAuth.ts +184 -0
- package/src/components/RedirectPage/RedirectPage.tsx +2 -2
- package/src/layouts/AuthLayout/AuthContext.tsx +2 -0
- package/src/layouts/AuthLayout/AuthLayout.tsx +22 -5
- package/src/layouts/AuthLayout/IdentifierForm.tsx +4 -0
- package/src/layouts/AuthLayout/OAuthCallback.tsx +172 -0
- package/src/layouts/AuthLayout/OAuthProviders.tsx +85 -0
- package/src/layouts/AuthLayout/index.ts +4 -0
- package/src/layouts/AuthLayout/types.ts +4 -0
- package/src/snippets/Analytics/events.ts +5 -0
- 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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
|
+
import { Loader2, AlertCircle, CheckCircle } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@djangocfg/ui/components';
|
|
8
|
+
|
|
9
|
+
import { useGithubAuth } from '../../auth/hooks';
|
|
10
|
+
|
|
11
|
+
export interface OAuthCallbackProps {
|
|
12
|
+
onSuccess?: (user: any, isNewUser: boolean, provider: string) => void;
|
|
13
|
+
onError?: (error: string) => void;
|
|
14
|
+
redirectUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type CallbackStatus = 'processing' | 'success' | 'error';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* OAuth Callback Handler Component
|
|
21
|
+
*
|
|
22
|
+
* Processes OAuth callback from providers (GitHub, etc.).
|
|
23
|
+
* Reads code, state, and provider from URL params and exchanges for tokens.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* Place this component on your /auth page to handle OAuth callbacks.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* // app/auth/page.tsx
|
|
31
|
+
* import { OAuthCallback, AuthLayout } from '@djangocfg/layouts';
|
|
32
|
+
*
|
|
33
|
+
* export default function AuthPage() {
|
|
34
|
+
* return (
|
|
35
|
+
* <>
|
|
36
|
+
* <OAuthCallback
|
|
37
|
+
* onSuccess={(user) => console.log('OAuth success:', user)}
|
|
38
|
+
* onError={(error) => console.error('OAuth error:', error)}
|
|
39
|
+
* />
|
|
40
|
+
* <AuthLayout enableGithubAuth>
|
|
41
|
+
* {/* Your auth content *\/}
|
|
42
|
+
* </AuthLayout>
|
|
43
|
+
* </>
|
|
44
|
+
* );
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export const OAuthCallback: React.FC<OAuthCallbackProps> = ({
|
|
49
|
+
onSuccess,
|
|
50
|
+
onError,
|
|
51
|
+
redirectUrl,
|
|
52
|
+
}) => {
|
|
53
|
+
const searchParams = useSearchParams();
|
|
54
|
+
const [status, setStatus] = useState<CallbackStatus | null>(null);
|
|
55
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
56
|
+
|
|
57
|
+
const provider = searchParams.get('provider');
|
|
58
|
+
const code = searchParams.get('code');
|
|
59
|
+
const state = searchParams.get('state');
|
|
60
|
+
const error = searchParams.get('error');
|
|
61
|
+
const errorDescription = searchParams.get('error_description');
|
|
62
|
+
|
|
63
|
+
const {
|
|
64
|
+
handleGithubCallback,
|
|
65
|
+
isLoading,
|
|
66
|
+
error: githubError,
|
|
67
|
+
} = useGithubAuth({
|
|
68
|
+
onSuccess: (user, isNewUser) => {
|
|
69
|
+
setStatus('success');
|
|
70
|
+
onSuccess?.(user, isNewUser, 'github');
|
|
71
|
+
},
|
|
72
|
+
onError: (err) => {
|
|
73
|
+
setStatus('error');
|
|
74
|
+
setErrorMessage(err);
|
|
75
|
+
onError?.(err);
|
|
76
|
+
},
|
|
77
|
+
redirectUrl,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Process OAuth callback on mount
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
// Check if this is an OAuth callback
|
|
83
|
+
if (!provider || !code || !state) {
|
|
84
|
+
// Not an OAuth callback, don't show anything
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for error from provider
|
|
89
|
+
if (error) {
|
|
90
|
+
setStatus('error');
|
|
91
|
+
setErrorMessage(errorDescription || error);
|
|
92
|
+
onError?.(errorDescription || error);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Process based on provider
|
|
97
|
+
const processCallback = async () => {
|
|
98
|
+
setStatus('processing');
|
|
99
|
+
|
|
100
|
+
if (provider === 'github') {
|
|
101
|
+
await handleGithubCallback(code, state);
|
|
102
|
+
} else {
|
|
103
|
+
setStatus('error');
|
|
104
|
+
setErrorMessage(`Unsupported OAuth provider: ${provider}`);
|
|
105
|
+
onError?.(`Unsupported OAuth provider: ${provider}`);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
processCallback();
|
|
110
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
111
|
+
}, [provider, code, state, error]);
|
|
112
|
+
|
|
113
|
+
// Don't render if not an OAuth callback
|
|
114
|
+
if (!provider || (!code && !error)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center">
|
|
120
|
+
<Card className="w-full max-w-md mx-4 shadow-lg">
|
|
121
|
+
<CardHeader className="text-center">
|
|
122
|
+
{status === 'processing' && (
|
|
123
|
+
<>
|
|
124
|
+
<div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-4">
|
|
125
|
+
<Loader2 className="w-6 h-6 text-primary animate-spin" />
|
|
126
|
+
</div>
|
|
127
|
+
<CardTitle>Signing you in...</CardTitle>
|
|
128
|
+
<CardDescription>
|
|
129
|
+
Please wait while we complete your {provider} authentication.
|
|
130
|
+
</CardDescription>
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{status === 'success' && (
|
|
135
|
+
<>
|
|
136
|
+
<div className="mx-auto w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mb-4">
|
|
137
|
+
<CheckCircle className="w-6 h-6 text-green-600 dark:text-green-400" />
|
|
138
|
+
</div>
|
|
139
|
+
<CardTitle>Success!</CardTitle>
|
|
140
|
+
<CardDescription>
|
|
141
|
+
You have been signed in successfully. Redirecting...
|
|
142
|
+
</CardDescription>
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{status === 'error' && (
|
|
147
|
+
<>
|
|
148
|
+
<div className="mx-auto w-12 h-12 bg-destructive/10 rounded-full flex items-center justify-center mb-4">
|
|
149
|
+
<AlertCircle className="w-6 h-6 text-destructive" />
|
|
150
|
+
</div>
|
|
151
|
+
<CardTitle>Authentication Failed</CardTitle>
|
|
152
|
+
<CardDescription className="text-destructive">
|
|
153
|
+
{errorMessage || githubError || 'An error occurred during authentication.'}
|
|
154
|
+
</CardDescription>
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</CardHeader>
|
|
158
|
+
|
|
159
|
+
{status === 'error' && (
|
|
160
|
+
<CardContent className="text-center">
|
|
161
|
+
<a
|
|
162
|
+
href="/auth"
|
|
163
|
+
className="text-primary hover:underline text-sm"
|
|
164
|
+
>
|
|
165
|
+
Try again
|
|
166
|
+
</a>
|
|
167
|
+
</CardContent>
|
|
168
|
+
)}
|
|
169
|
+
</Card>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Github, Loader2 } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
import { Button } from '@djangocfg/ui/components';
|
|
7
|
+
|
|
8
|
+
import { useGithubAuth } from '../../auth/hooks';
|
|
9
|
+
import { useAuthContext } from './AuthContext';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* OAuth Providers Component
|
|
13
|
+
*
|
|
14
|
+
* Shows OAuth login buttons (GitHub, etc.) when enabled.
|
|
15
|
+
* Handles the OAuth flow initiation.
|
|
16
|
+
*/
|
|
17
|
+
export const OAuthProviders: React.FC = () => {
|
|
18
|
+
const { enableGithubAuth, sourceUrl, error: contextError, setError } = useAuthContext();
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
isLoading: isGithubLoading,
|
|
22
|
+
error: githubError,
|
|
23
|
+
startGithubAuth,
|
|
24
|
+
} = useGithubAuth({
|
|
25
|
+
sourceUrl,
|
|
26
|
+
onError: (error) => {
|
|
27
|
+
setError(error);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Don't render if no OAuth providers are enabled
|
|
32
|
+
if (!enableGithubAuth) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const error = githubError || contextError;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="space-y-4">
|
|
40
|
+
{/* Divider */}
|
|
41
|
+
<div className="relative">
|
|
42
|
+
<div className="absolute inset-0 flex items-center">
|
|
43
|
+
<div className="w-full border-t border-border" />
|
|
44
|
+
</div>
|
|
45
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
46
|
+
<span className="bg-card px-2 text-muted-foreground">
|
|
47
|
+
Or continue with
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* OAuth Buttons */}
|
|
53
|
+
<div className="grid gap-3">
|
|
54
|
+
{enableGithubAuth && (
|
|
55
|
+
<Button
|
|
56
|
+
type="button"
|
|
57
|
+
variant="outline"
|
|
58
|
+
className="w-full h-11 text-base font-medium"
|
|
59
|
+
onClick={startGithubAuth}
|
|
60
|
+
disabled={isGithubLoading}
|
|
61
|
+
>
|
|
62
|
+
{isGithubLoading ? (
|
|
63
|
+
<div className="flex items-center gap-2">
|
|
64
|
+
<Loader2 className="w-5 h-5 animate-spin" />
|
|
65
|
+
Connecting...
|
|
66
|
+
</div>
|
|
67
|
+
) : (
|
|
68
|
+
<div className="flex items-center gap-2">
|
|
69
|
+
<Github className="w-5 h-5" />
|
|
70
|
+
Continue with GitHub
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</Button>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Error Message */}
|
|
78
|
+
{error && (
|
|
79
|
+
<div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
|
|
80
|
+
{error}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -12,6 +12,10 @@ export { AuthProvider as AuthLayoutProvider, useAuthContext } from './AuthContex
|
|
|
12
12
|
export { IdentifierForm } from './IdentifierForm';
|
|
13
13
|
export { OTPForm } from './OTPForm';
|
|
14
14
|
|
|
15
|
+
// OAuth
|
|
16
|
+
export { OAuthProviders } from './OAuthProviders';
|
|
17
|
+
export { OAuthCallback, type OAuthCallbackProps } from './OAuthCallback';
|
|
18
|
+
|
|
15
19
|
// Help component
|
|
16
20
|
export { AuthHelp } from './AuthHelp';
|
|
17
21
|
|
|
@@ -19,6 +19,7 @@ export interface AuthContextType {
|
|
|
19
19
|
privacyUrl?: string;
|
|
20
20
|
sourceUrl: string;
|
|
21
21
|
enablePhoneAuth?: boolean;
|
|
22
|
+
enableGithubAuth?: boolean;
|
|
22
23
|
|
|
23
24
|
// Form handlers
|
|
24
25
|
setIdentifier: (identifier: string) => void;
|
|
@@ -49,8 +50,11 @@ export interface AuthProps {
|
|
|
49
50
|
privacyUrl?: string;
|
|
50
51
|
className?: string;
|
|
51
52
|
enablePhoneAuth?: boolean; // Controls whether phone authentication is available
|
|
53
|
+
enableGithubAuth?: boolean; // Controls whether GitHub OAuth is available
|
|
54
|
+
redirectUrl?: string; // URL to redirect after successful auth (default: /dashboard)
|
|
52
55
|
onIdentifierSuccess?: (identifier: string, channel: 'email' | 'phone') => void;
|
|
53
56
|
onOTPSuccess?: () => void;
|
|
57
|
+
onOAuthSuccess?: (user: any, isNewUser: boolean, provider: string) => void;
|
|
54
58
|
onError?: (message: string) => void;
|
|
55
59
|
}
|
|
56
60
|
|
|
@@ -30,6 +30,11 @@ export const AnalyticsEvent = {
|
|
|
30
30
|
AUTH_TOKEN_REFRESH: 'auth_token_refresh',
|
|
31
31
|
AUTH_TOKEN_REFRESH_FAIL: 'auth_token_refresh_fail',
|
|
32
32
|
|
|
33
|
+
// OAuth Events
|
|
34
|
+
AUTH_OAUTH_START: 'auth_oauth_start',
|
|
35
|
+
AUTH_OAUTH_SUCCESS: 'auth_oauth_success',
|
|
36
|
+
AUTH_OAUTH_FAIL: 'auth_oauth_fail',
|
|
37
|
+
|
|
33
38
|
// Error Events
|
|
34
39
|
ERROR_BOUNDARY: 'error_boundary',
|
|
35
40
|
ERROR_API: 'error_api',
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
DialogHeader,
|
|
11
11
|
DialogTitle,
|
|
12
12
|
} from '@djangocfg/ui/components';
|
|
13
|
-
import { useEventListener,
|
|
13
|
+
import { useEventListener, useCfgRouter } from '@djangocfg/ui/hooks';
|
|
14
14
|
|
|
15
15
|
// Re-export events for backwards compatibility
|
|
16
16
|
export const DIALOG_EVENTS = {
|
|
@@ -31,7 +31,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
|
|
|
31
31
|
}) => {
|
|
32
32
|
const [open, setOpen] = useState(false);
|
|
33
33
|
const [message, setMessage] = useState<string>('Please sign in to continue');
|
|
34
|
-
const router =
|
|
34
|
+
const router = useCfgRouter();
|
|
35
35
|
|
|
36
36
|
// Listen for open auth dialog event
|
|
37
37
|
useEventListener(DIALOG_EVENTS.OPEN_AUTH_DIALOG, (payload: any) => {
|
|
@@ -66,20 +66,7 @@ export interface AIChatWidgetProps extends ChatWidgetConfig {
|
|
|
66
66
|
* Internal AI chat widget that uses context
|
|
67
67
|
*/
|
|
68
68
|
const AIChatWidgetInternal = React.memo<{ className?: string }>(({ className }) => {
|
|
69
|
-
const {
|
|
70
|
-
messages,
|
|
71
|
-
isLoading,
|
|
72
|
-
isMinimized,
|
|
73
|
-
config,
|
|
74
|
-
displayMode,
|
|
75
|
-
isMobile,
|
|
76
|
-
sendMessage,
|
|
77
|
-
openChat,
|
|
78
|
-
closeChat,
|
|
79
|
-
toggleMinimize,
|
|
80
|
-
setDisplayMode,
|
|
81
|
-
stopStreaming,
|
|
82
|
-
} = useAIChatContext();
|
|
69
|
+
const { config, displayMode, openChat } = useAIChatContext();
|
|
83
70
|
|
|
84
71
|
// Use layout hook for consistent positioning
|
|
85
72
|
const { getFabStyles, getFloatingStyles } = useChatLayout();
|
|
@@ -173,17 +160,7 @@ const AIChatWidgetInternal = React.memo<{ className?: string }>(({ className })
|
|
|
173
160
|
if (displayMode === 'sidebar') {
|
|
174
161
|
return (
|
|
175
162
|
<Portal>
|
|
176
|
-
<ChatSidebar
|
|
177
|
-
messages={messages}
|
|
178
|
-
isLoading={isLoading}
|
|
179
|
-
onSendMessage={sendMessage}
|
|
180
|
-
onClose={closeChat}
|
|
181
|
-
onModeChange={setDisplayMode}
|
|
182
|
-
onStopStreaming={stopStreaming}
|
|
183
|
-
title={config.title}
|
|
184
|
-
placeholder={config.placeholder}
|
|
185
|
-
greeting={config.greeting}
|
|
186
|
-
/>
|
|
163
|
+
<ChatSidebar />
|
|
187
164
|
</Portal>
|
|
188
165
|
);
|
|
189
166
|
}
|
|
@@ -192,20 +169,7 @@ const AIChatWidgetInternal = React.memo<{ className?: string }>(({ className })
|
|
|
192
169
|
return (
|
|
193
170
|
<Portal>
|
|
194
171
|
<div style={floatingStyles} className={className || ''}>
|
|
195
|
-
<ChatPanel
|
|
196
|
-
messages={messages}
|
|
197
|
-
isLoading={isLoading}
|
|
198
|
-
onSendMessage={sendMessage}
|
|
199
|
-
onClose={closeChat}
|
|
200
|
-
onMinimize={toggleMinimize}
|
|
201
|
-
onModeChange={setDisplayMode}
|
|
202
|
-
onStopStreaming={stopStreaming}
|
|
203
|
-
isMinimized={isMinimized}
|
|
204
|
-
isMobile={isMobile}
|
|
205
|
-
title={config.title}
|
|
206
|
-
placeholder={config.placeholder}
|
|
207
|
-
greeting={config.greeting}
|
|
208
|
-
/>
|
|
172
|
+
<ChatPanel />
|
|
209
173
|
</div>
|
|
210
174
|
</Portal>
|
|
211
175
|
);
|
|
@@ -93,8 +93,8 @@ export const ChatMessages = forwardRef<ChatMessagesHandle, ChatMessagesProps>(
|
|
|
93
93
|
const padding = largeGreetingIcon ? 'py-12' : 'py-8';
|
|
94
94
|
|
|
95
95
|
return (
|
|
96
|
-
<ScrollArea ref={scrollAreaRef} className="h-full">
|
|
97
|
-
<div className={`${isCompact ? 'p-3' : 'p-4'} space-y-4`}>
|
|
96
|
+
<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`}>
|
|
98
98
|
{/* Greeting */}
|
|
99
99
|
{messages.length === 0 && greeting && (
|
|
100
100
|
<div className={`text-center ${padding}`}>
|
|
@@ -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';
|