@djangocfg/layouts 2.1.18 → 2.1.20

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.18",
3
+ "version": "2.1.20",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -92,9 +92,9 @@
92
92
  "check": "tsc --noEmit"
93
93
  },
94
94
  "peerDependencies": {
95
- "@djangocfg/api": "^2.1.18",
96
- "@djangocfg/centrifugo": "^2.1.18",
97
- "@djangocfg/ui-nextjs": "^2.1.18",
95
+ "@djangocfg/api": "^2.1.20",
96
+ "@djangocfg/centrifugo": "^2.1.20",
97
+ "@djangocfg/ui-nextjs": "^2.1.20",
98
98
  "@hookform/resolvers": "^5.2.0",
99
99
  "consola": "^3.4.2",
100
100
  "lucide-react": "^0.545.0",
@@ -114,7 +114,7 @@
114
114
  "uuid": "^11.1.0"
115
115
  },
116
116
  "devDependencies": {
117
- "@djangocfg/typescript-config": "^2.1.18",
117
+ "@djangocfg/typescript-config": "^2.1.20",
118
118
  "@types/node": "^24.7.2",
119
119
  "@types/react": "^19.1.0",
120
120
  "@types/react-dom": "^19.1.0",
@@ -49,54 +49,48 @@ function getErrorIcon(code?: string | number): React.ReactNode {
49
49
  case '404':
50
50
  return (
51
51
  <svg
52
- className="w-24 h-24 mx-auto text-muted-foreground/50"
53
- fill="none"
54
- stroke="currentColor"
55
- viewBox="0 0 24 24"
52
+ className="w-32 h-32 mx-auto text-foreground/80"
53
+ viewBox="0 0 32 32"
54
+ fill="currentColor"
56
55
  aria-hidden="true"
57
56
  >
58
- {/* Missing Page Icon */}
59
- <path
60
- strokeLinecap="round"
61
- strokeLinejoin="round"
62
- strokeWidth={1.5}
63
- d="M9.343 3.07a7.227 7.227 0 0111.558 0c.806.515 1.393 1.39 1.393 2.37v6.636c0 .98-.587 1.855-1.393 2.37a7.227 7.227 0 01-11.558 0c-.806-.515-1.393-1.39-1.393-2.37V5.44c0-.98.587-1.855 1.393-2.37zM12 13a1 1 0 100-2 1 1 0 000 2z"
64
- />
57
+ {/* Cloud with 404 Icon */}
58
+ <g>
59
+ <path fillRule="evenodd" d="M19.889 21.734v-.947l-.631.947z" />
60
+ <path d="M15.484 19.636h1.032a.017.017 0 0 1 .017.017v3.1a.016.016 0 0 1-.016.016h-1.034a.016.016 0 0 1-.016-.016v-3.1a.017.017 0 0 1 .017-.017z" />
61
+ <g fillRule="evenodd">
62
+ <path d="M12.402 21.734v-.947l-.631.947z" />
63
+ <path d="M16 1.5A14.5 14.5 0 1 0 30.5 16 14.507 14.507 0 0 0 16 1.5zm-2.324 21.234H13.4v.532a.5.5 0 0 1-1 0v-.532h-1.563a.5.5 0 0 1-.416-.778l2.067-3.1a.5.5 0 0 1 .914.28v2.6h.274a.5.5 0 0 1 0 1zm-2.137-9.9A4.14 4.14 0 0 1 15.545 9.8a.5.5 0 0 1 0 1 3.138 3.138 0 0 0-3.04 2.3.5.5 0 0 1-.966-.259zm5.994 9.911a1.017 1.017 0 0 1-1.017 1.016h-1.032a1.017 1.017 0 0 1-1.017-1.016v-3.1a1.017 1.017 0 0 1 1.017-1.016h1.032a1.017 1.017 0 0 1 1.017 1.016zm3.63-.016h-.274v.532a.5.5 0 0 1-1 0v-.532h-1.565a.5.5 0 0 1-.416-.778l2.067-3.1a.5.5 0 0 1 .914.28v2.6h.274a.5.5 0 0 1 0 1zm1.036-1.52a.5.5 0 1 1-.4-.917 3.263 3.263 0 0 0-1.337-6.266.5.5 0 0 1-.5-.5 4.6 4.6 0 0 0-9.2 0 4.45 4.45 0 0 0 .153 1.161.5.5 0 0 1-.411.625 2.633 2.633 0 0 0-.364 5.148.5.5 0 0 1-.278.961 3.63 3.63 0 0 1-.024-6.984 5.467 5.467 0 0 1-.076-.911 5.6 5.6 0 0 1 11.182-.474 4.258 4.258 0 0 1 1.256 8.162z" />
64
+ </g>
65
+ </g>
65
66
  </svg>
66
67
  );
67
68
  case '500':
68
69
  return (
69
70
  <svg
70
- className="w-24 h-24 mx-auto text-muted-foreground/50"
71
- fill="none"
72
- stroke="currentColor"
73
- viewBox="0 0 24 24"
71
+ className="w-32 h-32 mx-auto text-foreground/80"
72
+ viewBox="0 0 512 512"
73
+ fill="currentColor"
74
74
  aria-hidden="true"
75
75
  >
76
- {/* Server Error Icon - Server with X */}
77
- <rect x="2" y="3" width="20" height="7" rx="1" strokeWidth={1.5} />
78
- <rect x="2" y="14" width="20" height="7" rx="1" strokeWidth={1.5} />
79
- <circle cx="6" cy="6.5" r="1" fill="currentColor" />
80
- <circle cx="6" cy="17.5" r="1" fill="currentColor" />
81
- <path strokeLinecap="round" strokeWidth={1.5} d="M22 2L2 22" />
76
+ {/* Server Error Icon - Circle with Exclamation */}
77
+ <g>
78
+ <path d="M256 118c-76.1 0-138 61.88-138 138s61.9 138.05 138 138.05 138-61.93 138-138S332.1 118 256 118zm0 237.93a30.12 30.12 0 1 1 30.11-30.12A30.15 30.15 0 0 1 256 355.88zm30.11-80.31a8.48 8.48 0 0 1-8.47 8.48h-43.28a8.48 8.48 0 0 1-8.47-8.48v-111a8.47 8.47 0 0 1 8.47-8.47h43.28a8.47 8.47 0 0 1 8.47 8.47z" />
79
+ <path d="M256 312.6a13.17 13.17 0 1 0 13.16 13.16A13.17 13.17 0 0 0 256 312.6zM242.84 173.07h26.32v94.02h-26.32z" />
80
+ <path d="M256 0C114.62 0 0 114.62 0 256s114.62 256 256 256 256-114.62 256-256S397.38 0 256 0zm109.57 365.6A155 155 0 1 1 411 256a153.91 153.91 0 0 1-45.43 109.6z" />
81
+ </g>
82
82
  </svg>
83
83
  );
84
84
  case '403':
85
85
  return (
86
86
  <svg
87
- className="w-24 h-24 mx-auto text-muted-foreground/50"
88
- fill="none"
89
- stroke="currentColor"
87
+ className="w-32 h-32 mx-auto text-foreground/80"
90
88
  viewBox="0 0 24 24"
89
+ fill="currentColor"
91
90
  aria-hidden="true"
92
91
  >
93
- {/* Forbidden Icon */}
94
- <path
95
- strokeLinecap="round"
96
- strokeLinejoin="round"
97
- strokeWidth={1.5}
98
- d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
99
- />
92
+ {/* Forbidden Icon - Circle with X */}
93
+ <path d="M12 1a11 11 0 1 0 11 11A11.013 11.013 0 0 0 12 1zm4.242 13.829a1 1 0 1 1-1.414 1.414L12 13.414l-2.828 2.829a1 1 0 0 1-1.414-1.414L10.586 12 7.758 9.171a1 1 0 1 1 1.414-1.414L12 10.586l2.828-2.829a1 1 0 1 1 1.414 1.414L13.414 12z" />
100
94
  </svg>
101
95
  );
102
96
  default:
@@ -148,18 +142,6 @@ export function ErrorLayout({
148
142
  return (
149
143
  <div className="min-h-screen flex items-center justify-center px-4 bg-background">
150
144
  <div className="max-w-2xl w-full text-center space-y-8">
151
- {/* Error Code */}
152
- {code && (
153
- <div className="relative">
154
- <h1
155
- className="text-[12rem] font-bold leading-none text-muted/20 select-none"
156
- aria-hidden="true"
157
- >
158
- {code}
159
- </h1>
160
- </div>
161
- )}
162
-
163
145
  {/* Illustration */}
164
146
  {finalIllustration && (
165
147
  <div className="flex justify-center py-8">
@@ -17,7 +17,7 @@
17
17
  * }}
18
18
  * header={{
19
19
  * title: 'Admin Dashboard',
20
- * profilePath: '/profile' // Optional
20
+ * profilePath: '/private/profile' // Optional
21
21
  * }}
22
22
  * >
23
23
  * {children}
@@ -39,7 +39,6 @@
39
39
  import React, { ReactNode, useMemo } from 'react';
40
40
  import { usePathname } from 'next/navigation';
41
41
  import { CentrifugoProvider } from '@djangocfg/centrifugo';
42
- import { ErrorBoundary } from '../../components/errors/ErrorBoundary';
43
42
  import { type AuthConfig } from '@djangocfg/api/auth';
44
43
  import { type ValidationErrorConfig, type CORSErrorConfig, type NetworkErrorConfig } from '../../components/errors/ErrorsTracker';
45
44
  import { AnalyticsProvider } from '../../snippets/Analytics';
@@ -151,7 +150,6 @@ function AppLayoutContent({
151
150
  adminLayout,
152
151
  analytics,
153
152
  centrifugo,
154
- errorBoundary,
155
153
  }: AppLayoutProps) {
156
154
  const pathname = usePathname();
157
155
 
@@ -165,8 +163,6 @@ function AppLayoutContent({
165
163
  [pathname, adminLayout, privateLayout, publicLayout]
166
164
  );
167
165
 
168
- const enableErrorBoundary = errorBoundary?.enabled !== false;
169
-
170
166
  // Render appropriate layout based on mode
171
167
  const renderLayout = () => {
172
168
  switch (layoutMode) {
@@ -240,20 +236,7 @@ function AppLayoutContent({
240
236
  {content}
241
237
  </CentrifugoProvider>
242
238
  );
243
-
244
-
245
- // Wrap with ErrorBoundary if enabled
246
- if (enableErrorBoundary) {
247
- return (
248
- <ErrorBoundary
249
- supportEmail={errorBoundary?.supportEmail}
250
- onError={errorBoundary?.onError}
251
- >
252
- {content}
253
- </ErrorBoundary>
254
- );
255
- }
256
-
239
+
257
240
  return content;
258
241
  }
259
242
 
@@ -261,13 +244,14 @@ function AppLayoutContent({
261
244
  * AppLayout - Main Component with All Providers
262
245
  */
263
246
  export function AppLayout(props: AppLayoutProps) {
264
- const { theme, auth, errorTracking } = props;
247
+ const { theme, auth, errorTracking, errorBoundary } = props;
265
248
 
266
249
  return (
267
250
  <BaseApp
268
251
  theme={theme}
269
252
  auth={auth}
270
253
  errorTracking={errorTracking}
254
+ errorBoundary={errorBoundary}
271
255
  >
272
256
  <AppLayoutContent {...props} />
273
257
  </BaseApp>
@@ -7,17 +7,25 @@
7
7
  * - SWRConfig (data fetching)
8
8
  * - AuthProvider (authentication context)
9
9
  * - ErrorTrackingProvider (error handling)
10
+ * - ErrorBoundary (React error boundary, enabled by default)
10
11
  *
11
12
  * @example
12
13
  * ```tsx
13
14
  * import { BaseApp } from '@djangocfg/layouts';
14
15
  *
16
+ * // ErrorBoundary enabled by default
15
17
  * <BaseApp
16
18
  * theme={{ defaultTheme: 'system' }}
17
19
  * auth={{ loginPath: '/auth/login' }}
20
+ * errorBoundary={{ supportEmail: 'support@example.com' }}
18
21
  * >
19
22
  * {children}
20
23
  * </BaseApp>
24
+ *
25
+ * // To disable ErrorBoundary:
26
+ * <BaseApp errorBoundary={{ enabled: false }}>
27
+ * {children}
28
+ * </BaseApp>
21
29
  * ```
22
30
  */
23
31
 
@@ -28,6 +36,7 @@ import { SWRConfig } from 'swr';
28
36
  import { ThemeProvider, Toaster, TooltipProvider } from '@djangocfg/ui-nextjs';
29
37
  import { AuthProvider, type AuthConfig } from '@djangocfg/api/auth';
30
38
  import { ErrorTrackingProvider, type ValidationErrorConfig, type CORSErrorConfig, type NetworkErrorConfig } from '../../components/errors/ErrorsTracker';
39
+ import { ErrorBoundary } from '../../components/errors/ErrorBoundary';
31
40
  import { PageProgress } from '../../components/core/PageProgress';
32
41
 
33
42
  export interface BaseAppProps {
@@ -56,12 +65,19 @@ export interface BaseAppProps {
56
65
  revalidateOnReconnect?: boolean;
57
66
  dedupingInterval?: number;
58
67
  };
68
+
69
+ /** Error boundary configuration (enabled by default) */
70
+ errorBoundary?: {
71
+ enabled?: boolean;
72
+ supportEmail?: string;
73
+ onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
74
+ };
59
75
  }
60
76
 
61
77
  /**
62
78
  * BaseApp - Core providers wrapper for any React/Next.js app
63
79
  *
64
- * Includes: ThemeProvider, TooltipProvider, SWRConfig, AuthProvider, ErrorTrackingProvider
80
+ * Includes: ThemeProvider, TooltipProvider, SWRConfig, AuthProvider, ErrorTrackingProvider, ErrorBoundary (optional)
65
81
  * Also renders global Toaster and PageProgress components
66
82
  */
67
83
  export function BaseApp({
@@ -69,9 +85,13 @@ export function BaseApp({
69
85
  theme,
70
86
  auth,
71
87
  errorTracking,
88
+ errorBoundary,
72
89
  swr,
73
90
  }: BaseAppProps) {
74
- return (
91
+ // ErrorBoundary is enabled by default
92
+ const enableErrorBoundary = errorBoundary?.enabled !== false;
93
+
94
+ const content = (
75
95
  <ThemeProvider
76
96
  defaultTheme={theme?.defaultTheme || 'system'}
77
97
  storageKey={theme?.storageKey}
@@ -100,4 +120,18 @@ export function BaseApp({
100
120
  </TooltipProvider>
101
121
  </ThemeProvider>
102
122
  );
123
+
124
+ // Wrap with ErrorBoundary only if explicitly enabled
125
+ if (enableErrorBoundary) {
126
+ return (
127
+ <ErrorBoundary
128
+ supportEmail={errorBoundary?.supportEmail}
129
+ onError={errorBoundary?.onError}
130
+ >
131
+ {content}
132
+ </ErrorBoundary>
133
+ );
134
+ }
135
+
136
+ return content;
103
137
  }
@@ -24,7 +24,7 @@
24
24
  * }}
25
25
  * header={{
26
26
  * title: 'Dashboard',
27
- * profilePath: '/profile' // Optional, defaults to '/profile'
27
+ * profilePath: '/private/profile' // Optional, defaults to '/private/profile'
28
28
  * }}
29
29
  * >
30
30
  * {children}
@@ -24,7 +24,7 @@ interface PrivateHeaderProps {
24
24
 
25
25
  export function PrivateHeader({ header }: PrivateHeaderProps) {
26
26
  const { user, logout } = useAuth();
27
- const profilePath = header?.profilePath || '/profile';
27
+ const profilePath = header?.profilePath || '/private/profile';
28
28
 
29
29
  return (
30
30
  <header
@@ -34,8 +34,8 @@ export interface UserMenuProps {
34
34
 
35
35
  export function UserMenu({
36
36
  variant = 'desktop',
37
- profilePath = '/profile',
38
- dashboardPath = '/dashboard',
37
+ profilePath = '/private/profile',
38
+ dashboardPath = '/private',
39
39
  authPath = '/auth',
40
40
  }: UserMenuProps) {
41
41
  const { user, isAuthenticated, logout } = useAuth();
@@ -0,0 +1,441 @@
1
+ # MCP Chat - AI Assistant Integration
2
+
3
+ AI-powered chat widget with global trigger system for seamless integration throughout your application.
4
+
5
+ ## Features
6
+
7
+ - 🎯 **Global Trigger System** - Call chat from anywhere using `useMcpChat()` hook
8
+ - 🤖 **Simple AI Button** - Universal `AskAIButton` component with AI assistant icon
9
+ - 💬 **Multiple Display Modes** - Floating panel, sidebar, or closed
10
+ - 🔄 **Streaming Responses** - Real-time AI responses
11
+ - 📱 **Mobile Responsive** - Optimized for all devices
12
+ - 🎭 **Context-Aware** - Send structured context with messages
13
+ - 🔌 **Easy Integration** - Drop-in component with minimal setup
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Setup Provider (once in your app)
18
+
19
+ ```tsx
20
+ // app/layout.tsx
21
+ import { AIChatProvider, AIChatWidget } from '@djangocfg/layouts/snippets/McpChat';
22
+
23
+ export default function RootLayout({ children }) {
24
+ return (
25
+ <html>
26
+ <body>
27
+ <AIChatProvider>
28
+ {children}
29
+ <AIChatWidget />
30
+ </AIChatProvider>
31
+ </body>
32
+ </html>
33
+ );
34
+ }
35
+ ```
36
+
37
+ ### 2. Use Anywhere
38
+
39
+ #### Option A: Ready-to-use Button (Recommended)
40
+
41
+ ```tsx
42
+ import { AskAIButton } from '@djangocfg/layouts/snippets/McpChat';
43
+
44
+ // Simple usage
45
+ <AskAIButton message="Explain this feature">
46
+ Ask AI
47
+ </AskAIButton>
48
+
49
+ // With context data
50
+ <AskAIButton
51
+ message="Why is this failing?"
52
+ contextData={{ error: error.stack, component: 'UserForm' }}
53
+ source="ErrorBoundary"
54
+ >
55
+ Explain error
56
+ </AskAIButton>
57
+
58
+ // Custom styling
59
+ <AskAIButton
60
+ message="Help me optimize this"
61
+ contextData={{ topic: "database-performance" }}
62
+ variant="default"
63
+ size="lg"
64
+ >
65
+ Get advice
66
+ </AskAIButton>
67
+ ```
68
+
69
+ #### Option B: Custom Hook
70
+
71
+ ```tsx
72
+ import { useMcpChat } from '@djangocfg/layouts/snippets/McpChat';
73
+
74
+ function MyComponent() {
75
+ const { sendToChat } = useMcpChat();
76
+
77
+ const handleClick = () => {
78
+ sendToChat({
79
+ message: 'Explain this error',
80
+ context: {
81
+ data: { error: errorDetails },
82
+ source: 'MyComponent'
83
+ }
84
+ });
85
+ };
86
+
87
+ return <button onClick={handleClick}>Ask AI</button>;
88
+ }
89
+ ```
90
+
91
+ ## Components
92
+
93
+ ### AskAIButton
94
+
95
+ Universal AI chat trigger button with Bot icon:
96
+
97
+ ```tsx
98
+ <AskAIButton
99
+ message="Your question or prompt"
100
+ contextData={{ /* optional context */ }}
101
+ source="ComponentName"
102
+ variant="outline"
103
+ size="default"
104
+ >
105
+ Button text
106
+ </AskAIButton>
107
+ ```
108
+
109
+ **Props:**
110
+ - `message` - Question or prompt to send to AI (required)
111
+ - `contextData` - Additional context object (optional)
112
+ - `source` - Source component name for tracking (optional)
113
+ - `autoSend` - Auto-send message after opening (default: `true`)
114
+ - `showIcon` - Show Bot icon (default: `true`)
115
+ - All standard Button props (`variant`, `size`, `className`, etc.)
116
+
117
+ ### useMcpChat Hook
118
+
119
+ Custom hook for programmatic chat triggering:
120
+
121
+ ```tsx
122
+ const { sendToChat, isChatAvailable } = useMcpChat();
123
+
124
+ sendToChat({
125
+ message: 'Your message',
126
+ context: {
127
+ data: { /* contextual data */ },
128
+ source: 'ComponentName'
129
+ },
130
+ autoSend: true,
131
+ displayMode: 'floating' | 'sidebar'
132
+ });
133
+ ```
134
+
135
+ ## Real-World Examples
136
+
137
+ ### Error Handling
138
+
139
+ ```tsx
140
+ function ErrorBoundary({ error, children }) {
141
+ if (error) {
142
+ return (
143
+ <div>
144
+ <h2>Error: {error.message}</h2>
145
+ <AskAIButton
146
+ message="Explain this error and how to fix it"
147
+ contextData={{
148
+ error: error.message,
149
+ stack: error.stack,
150
+ component: 'UserForm'
151
+ }}
152
+ source="ErrorBoundary"
153
+ >
154
+ Explain error
155
+ </AskAIButton>
156
+ </div>
157
+ );
158
+ }
159
+ return children;
160
+ }
161
+ ```
162
+
163
+ ### Feature Documentation
164
+
165
+ ```tsx
166
+ function FeatureCard({ feature }) {
167
+ return (
168
+ <Card>
169
+ <CardHeader>{feature.name}</CardHeader>
170
+ <CardContent>{feature.description}</CardContent>
171
+ <CardFooter>
172
+ <AskAIButton
173
+ message={`Explain how ${feature.name} works`}
174
+ contextData={{
175
+ feature: feature.name,
176
+ version: feature.version
177
+ }}
178
+ source="FeatureCard"
179
+ >
180
+ Learn more
181
+ </AskAIButton>
182
+ </CardFooter>
183
+ </Card>
184
+ );
185
+ }
186
+ ```
187
+
188
+ ### Settings Page
189
+
190
+ ```tsx
191
+ function DatabaseSettings() {
192
+ return (
193
+ <div>
194
+ <h3>Database Configuration</h3>
195
+ <form>{/* form fields */}</form>
196
+ <AskAIButton
197
+ message="What are the best practices for PostgreSQL optimization?"
198
+ contextData={{
199
+ topic: "database-optimization",
200
+ dbType: "PostgreSQL"
201
+ }}
202
+ source="DatabaseSettings"
203
+ >
204
+ Get tips
205
+ </AskAIButton>
206
+ </div>
207
+ );
208
+ }
209
+ ```
210
+
211
+ ### Pricing Comparison
212
+
213
+ ```tsx
214
+ function PricingSection() {
215
+ return (
216
+ <div>
217
+ {/* pricing cards */}
218
+ <AskAIButton
219
+ message="Which package is right for my team?"
220
+ contextData={{
221
+ packages: ["starter", "professional", "enterprise"],
222
+ intent: "decision-making"
223
+ }}
224
+ source="PricingSection"
225
+ >
226
+ Which package?
227
+ </AskAIButton>
228
+ </div>
229
+ );
230
+ }
231
+ ```
232
+
233
+ ### Installation Help
234
+
235
+ ```tsx
236
+ function QuickStart() {
237
+ return (
238
+ <div>
239
+ <h2>Installation</h2>
240
+ <pre>{installCommands}</pre>
241
+ <AskAIButton
242
+ message="I need help with Django-CFG installation"
243
+ contextData={{
244
+ topic: "installation",
245
+ os: detectedOS
246
+ }}
247
+ source="QuickStart"
248
+ >
249
+ Installation help?
250
+ </AskAIButton>
251
+ </div>
252
+ );
253
+ }
254
+ ```
255
+
256
+ ## Architecture
257
+
258
+ ```
259
+ ┌─────────────────────────────────────┐
260
+ │ Any Component │
261
+ │ ┌──────────────────────────────┐ │
262
+ │ │ AskAIButton / useMcpChat() │ │
263
+ │ │ sendToChat({ message, ... }) │ │
264
+ │ └──────────────┬───────────────┘ │
265
+ └─────────────────┼───────────────────┘
266
+
267
+ │ CustomEvent
268
+ │ 'mcp:chat:send'
269
+
270
+ ┌─────────────────────────────────────┐
271
+ │ AIChatProvider (Event Listener) │
272
+ │ ┌──────────────────────────────┐ │
273
+ │ │ 1. Receives event │ │
274
+ │ │ 2. Sends confirmation │ │
275
+ │ │ 3. Opens chat │ │
276
+ │ │ 4. Sends message to AI │ │
277
+ │ └──────────────────────────────┘ │
278
+ └─────────────────────────────────────┘
279
+
280
+
281
+ ┌─────────────────────────────────────┐
282
+ │ AIChatWidget (UI) │
283
+ │ - Floating panel or sidebar │
284
+ │ - Message display │
285
+ │ - Streaming responses │
286
+ └─────────────────────────────────────┘
287
+ ```
288
+
289
+ ## API
290
+
291
+ ### AskAIButton Props
292
+
293
+ ```tsx
294
+ interface AskAIButtonProps extends ButtonProps {
295
+ /** Message to send to AI */
296
+ message: string;
297
+ /** Additional context data */
298
+ contextData?: Record<string, any>;
299
+ /** Source component name */
300
+ source?: string;
301
+ /** Auto-send message (default: true) */
302
+ autoSend?: boolean;
303
+ /** Show icon (default: true) */
304
+ showIcon?: boolean;
305
+ /** Callback after sending */
306
+ onSent?: () => void;
307
+ }
308
+ ```
309
+
310
+ ### McpChatEventDetail
311
+
312
+ ```tsx
313
+ interface McpChatEventDetail {
314
+ message: string;
315
+ context?: {
316
+ data?: Record<string, any>;
317
+ source?: string;
318
+ };
319
+ autoSend?: boolean;
320
+ displayMode?: 'floating' | 'sidebar' | 'closed';
321
+ }
322
+ ```
323
+
324
+ ### UseMcpChatReturn
325
+
326
+ ```tsx
327
+ interface UseMcpChatReturn {
328
+ /** Send message to chat */
329
+ sendToChat: (detail: McpChatEventDetail) => void;
330
+ /** Check if chat is available */
331
+ isChatAvailable: () => boolean;
332
+ }
333
+ ```
334
+
335
+ ## Best Practices
336
+
337
+ 1. **Always provide source** - Helps track where requests come from
338
+ ```tsx
339
+ <AskAIButton message="..." source="ComponentName">
340
+ ```
341
+
342
+ 2. **Include contextData** - More context = better AI responses
343
+ ```tsx
344
+ <AskAIButton
345
+ message="..."
346
+ contextData={{ feature: "auth", version: "2.0" }}
347
+ >
348
+ ```
349
+
350
+ 3. **Keep messages clear** - Write clear questions or prompts
351
+ ```tsx
352
+ // Good
353
+ message="Why is Django faster than FastAPI in enterprise scenarios?"
354
+
355
+ // Too vague
356
+ message="Explain this"
357
+ ```
358
+
359
+ 4. **Short button labels** - Keep button text concise (max 3 words)
360
+ ```tsx
361
+ <AskAIButton message="...">
362
+ Explain this {/* Good */}
363
+ </AskAIButton>
364
+ ```
365
+
366
+ 5. **Check availability** - Use `isChatAvailable()` for conditional UI
367
+ ```tsx
368
+ const { isChatAvailable } = useMcpChat();
369
+ if (!isChatAvailable()) {
370
+ return <p>Chat loading...</p>;
371
+ }
372
+ ```
373
+
374
+ ## Error Handling
375
+
376
+ If chat is not available:
377
+ - Console error logged
378
+ - Alert shown to user
379
+ - Button automatically disabled
380
+
381
+ ```tsx
382
+ const { sendToChat, isChatAvailable } = useMcpChat();
383
+
384
+ if (!isChatAvailable()) {
385
+ console.log('Chat not loaded');
386
+ }
387
+ ```
388
+
389
+ ## TypeScript Support
390
+
391
+ Fully typed with TypeScript for excellent IDE support:
392
+
393
+ ```tsx
394
+ import type {
395
+ McpChatEventDetail,
396
+ UseMcpChatReturn,
397
+ AskAIButtonProps
398
+ } from '@djangocfg/layouts/snippets/McpChat';
399
+ ```
400
+
401
+ ## Common Use Cases
402
+
403
+ ### Documentation Pages
404
+ ```tsx
405
+ <AskAIButton message="How do I set this up?">
406
+ Setup help?
407
+ </AskAIButton>
408
+ ```
409
+
410
+ ### Comparison Tables
411
+ ```tsx
412
+ <AskAIButton message="Why is option A better than option B?">
413
+ Explain difference
414
+ </AskAIButton>
415
+ ```
416
+
417
+ ### Feature Sections
418
+ ```tsx
419
+ <AskAIButton message="Which features should I explore first?">
420
+ Explore features?
421
+ </AskAIButton>
422
+ ```
423
+
424
+ ### Benchmarks
425
+ ```tsx
426
+ <AskAIButton message="Why is this faster in production?">
427
+ Explain benchmarks?
428
+ </AskAIButton>
429
+ ```
430
+
431
+ ## Contributing
432
+
433
+ When adding new features:
434
+ 1. Update type definitions in `types.ts`
435
+ 2. Add examples to this README
436
+ 3. Test with different scenarios
437
+ 4. Keep the API simple and consistent
438
+
439
+ ## License
440
+
441
+ MIT - Part of DjangoCFG project
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+
3
+ import { Button, type ButtonProps } from '@djangocfg/ui-nextjs';
4
+ import { Bot } from 'lucide-react';
5
+ import { useMcpChat } from '../hooks/useMcpChat';
6
+ import type { McpChatEventDetail } from '../types';
7
+
8
+ export interface AskAIButtonProps extends Omit<ButtonProps, 'onClick'> {
9
+ /** Message to send to AI */
10
+ message: string;
11
+ /** Additional context data */
12
+ contextData?: Record<string, any>;
13
+ /** Source component name */
14
+ source?: string;
15
+ /** Auto-send message (default: true) */
16
+ autoSend?: boolean;
17
+ /** Show icon (default: true) */
18
+ showIcon?: boolean;
19
+ /** Callback after sending */
20
+ onSent?: () => void;
21
+ }
22
+
23
+ /**
24
+ * Universal AI chat trigger button
25
+ *
26
+ * @example Basic usage
27
+ * ```tsx
28
+ * <AskAIButton message="Explain this feature">
29
+ * Explain this
30
+ * </AskAIButton>
31
+ * ```
32
+ *
33
+ * @example With context
34
+ * ```tsx
35
+ * <AskAIButton
36
+ * message="Why is this failing?"
37
+ * contextData={{ error: error.stack }}
38
+ * source="ErrorBoundary"
39
+ * >
40
+ * Ask AI
41
+ * </AskAIButton>
42
+ * ```
43
+ */
44
+ export function AskAIButton({
45
+ message,
46
+ contextData,
47
+ source,
48
+ autoSend = true,
49
+ showIcon = true,
50
+ onSent,
51
+ children = 'Ask AI',
52
+ variant = 'outline',
53
+ size = 'default',
54
+ className,
55
+ ...buttonProps
56
+ }: AskAIButtonProps) {
57
+ const { sendToChat, isChatAvailable } = useMcpChat();
58
+
59
+ const handleClick = () => {
60
+ const detail: McpChatEventDetail = {
61
+ message,
62
+ autoSend,
63
+ };
64
+
65
+ if (contextData || source) {
66
+ detail.context = {
67
+ data: contextData,
68
+ source,
69
+ };
70
+ }
71
+
72
+ sendToChat(detail);
73
+ onSent?.();
74
+ };
75
+
76
+ const isAvailable = isChatAvailable();
77
+
78
+ return (
79
+ <Button
80
+ onClick={handleClick}
81
+ variant={variant}
82
+ size={size}
83
+ className={className}
84
+ disabled={!isAvailable}
85
+ title={!isAvailable ? 'AI Chat not available' : undefined}
86
+ {...buttonProps}
87
+ >
88
+ {showIcon && <Bot className="h-4 w-4 mr-2" />}
89
+ {children}
90
+ </Button>
91
+ );
92
+ }
@@ -19,3 +19,6 @@ export type { AIMessageInputProps } from './MessageInput';
19
19
 
20
20
  export { ChatSidebar } from './ChatSidebar';
21
21
  export type { ChatSidebarProps } from './ChatSidebar';
22
+
23
+ export { AskAIButton } from './AskAIButton';
24
+ export type { AskAIButtonProps } from './AskAIButton';
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { createContext, useContext, useState, useCallback, useMemo, useEffect, type ReactNode } from 'react';
3
+ import { createContext, useContext, useState, useCallback, useMemo, useEffect, type ReactNode } from 'react';
4
4
  import { useLocalStorage, useIsMobile } from '@djangocfg/ui-nextjs/hooks';
5
5
  import { useAIChat } from '../hooks/useAIChat';
6
6
  import { mcpEndpoints, type AIChatMessage, type ChatWidgetConfig, type ChatDisplayMode } from '../types';
@@ -223,6 +223,67 @@ export function AIChatProvider({
223
223
  ]
224
224
  );
225
225
 
226
+ // =============================================================================
227
+ // Global Chat Event Listener
228
+ // =============================================================================
229
+
230
+ /**
231
+ * Listen for mcp:chat:send events from useMcpChat hook
232
+ * This allows any component to trigger chat from anywhere in the app
233
+ */
234
+ useEffect(() => {
235
+ // Register chat as available
236
+ if (typeof window !== 'undefined') {
237
+ (window as any).__MCP_CHAT_AVAILABLE__ = true;
238
+ }
239
+
240
+ const handleChatEvent = (event: Event) => {
241
+ const customEvent = event as CustomEvent<import('../types').McpChatEventDetail>;
242
+ const { message, context, autoSend = true, displayMode: requestedMode } = customEvent.detail;
243
+
244
+ // Send confirmation that event was handled
245
+ window.dispatchEvent(new CustomEvent('mcp:chat:handled'));
246
+
247
+ // Format message with context if provided
248
+ let fullMessage = message;
249
+ if (context) {
250
+ if (context.type) {
251
+ fullMessage = `[${context.type.toUpperCase()}] ${message}`;
252
+ }
253
+ if (context.data) {
254
+ fullMessage += `\n\n**Context:**\n\`\`\`json\n${JSON.stringify(context.data, null, 2)}\n\`\`\``;
255
+ }
256
+ if (context.source) {
257
+ fullMessage += `\n\n_Source: ${context.source}_`;
258
+ }
259
+ }
260
+
261
+ // Open chat in requested mode (or default to floating)
262
+ if (requestedMode) {
263
+ setDisplayMode(requestedMode);
264
+ } else {
265
+ openChat();
266
+ }
267
+
268
+ // Auto-send message if requested
269
+ if (autoSend) {
270
+ // Small delay to ensure chat is open and ready
271
+ setTimeout(() => {
272
+ sendMessage(fullMessage);
273
+ }, 100);
274
+ }
275
+ };
276
+
277
+ window.addEventListener('mcp:chat:send', handleChatEvent);
278
+
279
+ return () => {
280
+ window.removeEventListener('mcp:chat:send', handleChatEvent);
281
+ if (typeof window !== 'undefined') {
282
+ (window as any).__MCP_CHAT_AVAILABLE__ = false;
283
+ }
284
+ };
285
+ }, [sendMessage, openChat, setDisplayMode]);
286
+
226
287
  return <AIChatContext.Provider value={value}>{children}</AIChatContext.Provider>;
227
288
  }
228
289
 
@@ -2,4 +2,5 @@
2
2
 
3
3
  export { useAIChat } from './useAIChat';
4
4
  export { useChatLayout } from './useChatLayout';
5
+ export { useMcpChat } from './useMcpChat';
5
6
  export type { ChatLayoutConfig, UseChatLayoutReturn } from './useChatLayout';
@@ -0,0 +1,89 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+ import type { McpChatEventDetail, UseMcpChatReturn } from '../types';
5
+
6
+ /**
7
+ * Hook to send messages to MCP Chat from anywhere in the app
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * function ErrorBoundary({ error }) {
12
+ * const { sendToChat } = useMcpChat();
13
+ *
14
+ * const explainError = () => {
15
+ * sendToChat({
16
+ * message: `Explain this error: ${error.message}`,
17
+ * context: {
18
+ * type: 'error',
19
+ * data: { error: error.stack },
20
+ * source: 'ErrorBoundary'
21
+ * }
22
+ * });
23
+ * };
24
+ *
25
+ * return <button onClick={explainError}>Explain Error</button>;
26
+ * }
27
+ * ```
28
+ */
29
+ export function useMcpChat(): UseMcpChatReturn {
30
+ /**
31
+ * Send message to chat via CustomEvent
32
+ */
33
+ const sendToChat = useCallback((detail: McpChatEventDetail) => {
34
+ if (typeof window === 'undefined') {
35
+ console.error('[useMcpChat] Cannot send message: window is not available');
36
+ return;
37
+ }
38
+
39
+ // Create custom event
40
+ const event = new CustomEvent('mcp:chat:send', {
41
+ detail,
42
+ bubbles: true,
43
+ });
44
+
45
+ // Set up handler confirmation listener
46
+ let handled = false;
47
+ const handleConfirmation = () => {
48
+ handled = true;
49
+ };
50
+
51
+ window.addEventListener('mcp:chat:handled', handleConfirmation, { once: true });
52
+
53
+ // Dispatch event
54
+ window.dispatchEvent(event);
55
+
56
+ // Check if event was handled
57
+ setTimeout(() => {
58
+ window.removeEventListener('mcp:chat:handled', handleConfirmation);
59
+
60
+ if (!handled) {
61
+ const errorMessage = 'AI Chat is not available. Please make sure the chat component is loaded.';
62
+ console.error('[useMcpChat]', errorMessage);
63
+
64
+ // Import consola dynamically if available
65
+ if (typeof window !== 'undefined' && (window as any).consola) {
66
+ (window as any).consola.error('[useMcpChat] Chat not available');
67
+ }
68
+
69
+ // Show user-friendly alert
70
+ alert(errorMessage);
71
+ }
72
+ }, 100);
73
+ }, []);
74
+
75
+ /**
76
+ * Check if chat is available
77
+ */
78
+ const isChatAvailable = useCallback(() => {
79
+ if (typeof window === 'undefined') return false;
80
+
81
+ // Check if chat registered itself
82
+ return (window as any).__MCP_CHAT_AVAILABLE__ === true;
83
+ }, []);
84
+
85
+ return {
86
+ sendToChat,
87
+ isChatAvailable,
88
+ };
89
+ }
@@ -42,12 +42,13 @@
42
42
  */
43
43
 
44
44
  // Components
45
- export { ChatWidget, AIChatWidget, ChatPanel, MessageBubble, AIMessageInput } from './components';
45
+ export { ChatWidget, AIChatWidget, ChatPanel, MessageBubble, AIMessageInput, AskAIButton } from './components';
46
46
  export type {
47
47
  ChatWidgetProps,
48
48
  AIChatWidgetProps,
49
49
  MessageBubbleProps,
50
50
  AIMessageInputProps,
51
+ AskAIButtonProps,
51
52
  } from './components';
52
53
 
53
54
  // Context
@@ -60,7 +61,7 @@ export type {
60
61
  } from './context';
61
62
 
62
63
  // Hooks
63
- export { useAIChat, useChatLayout } from './hooks';
64
+ export { useAIChat, useChatLayout, useMcpChat } from './hooks';
64
65
  export type { ChatLayoutConfig, UseChatLayoutReturn } from './hooks';
65
66
 
66
67
  // Types
@@ -72,4 +73,7 @@ export type {
72
73
  UseAIChatOptions,
73
74
  UseAIChatReturn,
74
75
  ChatWidgetConfig,
76
+ McpChatContextType,
77
+ McpChatEventDetail,
78
+ UseMcpChatReturn,
75
79
  } from './types';
@@ -141,3 +141,49 @@ export interface UseAIChatReturn {
141
141
  clearMessages: () => void;
142
142
  stopStreaming: () => void;
143
143
  }
144
+
145
+ // =============================================================================
146
+ // MCP Chat Events (Global Chat Trigger)
147
+ // =============================================================================
148
+
149
+ /**
150
+ * Context type for chat messages triggered from different parts of the app
151
+ */
152
+ export type McpChatContextType =
153
+ | 'error' // Error explanation
154
+ | 'question' // General question
155
+ | 'explain' // Explain something
156
+ | 'advice' // Get advice
157
+ | 'help' // Help request
158
+ | 'custom'; // Custom context
159
+
160
+ /**
161
+ * Event detail for mcp:chat:send custom event
162
+ */
163
+ export interface McpChatEventDetail {
164
+ /** Message to send to chat */
165
+ message: string;
166
+ /** Optional context about the message */
167
+ context?: {
168
+ /** Type of context */
169
+ type?: McpChatContextType;
170
+ /** Additional data (error object, selected element, etc.) */
171
+ data?: Record<string, any>;
172
+ /** Source component/page that triggered the event */
173
+ source?: string;
174
+ };
175
+ /** Auto-send message after opening chat (default: true) */
176
+ autoSend?: boolean;
177
+ /** Open chat in specific mode (default: 'floating') */
178
+ displayMode?: ChatDisplayMode;
179
+ }
180
+
181
+ /**
182
+ * useMcpChat hook return type
183
+ */
184
+ export interface UseMcpChatReturn {
185
+ /** Send message to chat from anywhere in the app */
186
+ sendToChat: (detail: McpChatEventDetail) => void;
187
+ /** Check if chat is available */
188
+ isChatAvailable: () => boolean;
189
+ }
@@ -18,6 +18,8 @@ export {
18
18
  useAIChatContextOptional,
19
19
  useAIChat,
20
20
  useChatLayout,
21
+ useMcpChat,
22
+ AskAIButton,
21
23
  } from './McpChat';
22
24
  export type {
23
25
  AIChatWidgetProps,
@@ -36,4 +38,8 @@ export type {
36
38
  UseAIChatOptions,
37
39
  UseAIChatReturn,
38
40
  ChatWidgetConfig,
41
+ McpChatContextType,
42
+ McpChatEventDetail,
43
+ UseMcpChatReturn,
44
+ AskAIButtonProps,
39
45
  } from './McpChat';