@djangocfg/layouts 2.0.6 → 2.0.8

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.
Files changed (37) hide show
  1. package/README.md +65 -6
  2. package/package.json +14 -13
  3. package/src/auth/hooks/index.ts +1 -0
  4. package/src/auth/hooks/useGithubAuth.ts +183 -0
  5. package/src/layouts/AuthLayout/AuthContext.tsx +2 -0
  6. package/src/layouts/AuthLayout/AuthLayout.tsx +22 -5
  7. package/src/layouts/AuthLayout/IdentifierForm.tsx +4 -0
  8. package/src/layouts/AuthLayout/OAuthCallback.tsx +172 -0
  9. package/src/layouts/AuthLayout/OAuthProviders.tsx +85 -0
  10. package/src/layouts/AuthLayout/index.ts +4 -0
  11. package/src/layouts/AuthLayout/types.ts +4 -0
  12. package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +3 -22
  13. package/src/layouts/SupportLayout/components/MessageList.tsx +1 -1
  14. package/src/layouts/SupportLayout/components/TicketList.tsx +1 -1
  15. package/src/snippets/Analytics/events.ts +5 -0
  16. package/src/snippets/Chat/components/MessageList.tsx +1 -1
  17. package/src/snippets/Chat/components/SessionList.tsx +1 -1
  18. package/src/snippets/McpChat/components/AIChatWidget.tsx +268 -0
  19. package/src/snippets/McpChat/components/ChatMessages.tsx +151 -0
  20. package/src/snippets/McpChat/components/ChatPanel.tsx +126 -0
  21. package/src/snippets/McpChat/components/ChatSidebar.tsx +119 -0
  22. package/src/snippets/McpChat/components/ChatWidget.tsx +134 -0
  23. package/src/snippets/McpChat/components/MessageBubble.tsx +125 -0
  24. package/src/snippets/McpChat/components/MessageInput.tsx +139 -0
  25. package/src/snippets/McpChat/components/index.ts +22 -0
  26. package/src/snippets/McpChat/config.ts +35 -0
  27. package/src/snippets/McpChat/context/AIChatContext.tsx +245 -0
  28. package/src/snippets/McpChat/context/ChatContext.tsx +350 -0
  29. package/src/snippets/McpChat/context/index.ts +7 -0
  30. package/src/snippets/McpChat/hooks/index.ts +5 -0
  31. package/src/snippets/McpChat/hooks/useAIChat.ts +487 -0
  32. package/src/snippets/McpChat/hooks/useChatLayout.ts +329 -0
  33. package/src/snippets/McpChat/index.ts +76 -0
  34. package/src/snippets/McpChat/types.ts +141 -0
  35. package/src/snippets/index.ts +32 -0
  36. package/src/utils/index.ts +0 -1
  37. package/src/utils/og-image.ts +0 -169
@@ -0,0 +1,329 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useCallback, useRef } from 'react';
4
+ import type { ChatDisplayMode } from '../types';
5
+
6
+ /**
7
+ * Configuration for chat layout management
8
+ */
9
+ export interface ChatLayoutConfig {
10
+ /** Width of sidebar in pixels */
11
+ sidebarWidth?: number;
12
+ /** Z-index for chat elements */
13
+ zIndex?: number;
14
+ /** Animation duration in ms */
15
+ animationDuration?: number;
16
+ /** Element to push (defaults to body) */
17
+ pushTarget?: 'body' | 'main' | string;
18
+ }
19
+
20
+ /**
21
+ * Return type for useChatLayout hook
22
+ */
23
+ export interface UseChatLayoutReturn {
24
+ /** Apply layout changes for mode */
25
+ applyLayout: (mode: ChatDisplayMode) => void;
26
+ /** Reset layout to default */
27
+ resetLayout: () => void;
28
+ /** Get CSS for sidebar container */
29
+ getSidebarStyles: () => React.CSSProperties;
30
+ /** Get CSS for floating container */
31
+ getFloatingStyles: (position: 'bottom-right' | 'bottom-left') => React.CSSProperties;
32
+ /** Get CSS for FAB button */
33
+ getFabStyles: (position: 'bottom-right' | 'bottom-left') => React.CSSProperties;
34
+ }
35
+
36
+ const DEFAULT_CONFIG: Required<ChatLayoutConfig> = {
37
+ sidebarWidth: 400,
38
+ zIndex: 300,
39
+ animationDuration: 200,
40
+ pushTarget: 'body',
41
+ };
42
+
43
+ /** Stored original styles for fixed elements */
44
+ interface FixedElementOriginalStyles {
45
+ element: HTMLElement;
46
+ right: string;
47
+ transition: string;
48
+ }
49
+
50
+ /**
51
+ * Hook for managing chat layout embedding modes
52
+ *
53
+ * Handles:
54
+ * - Sidebar mode: pushes content left by adding margin to target element
55
+ * AND automatically adjusts all position:fixed elements with right:0
56
+ * - Floating mode: positions chat at bottom-right/left
57
+ * - Closed mode: just shows FAB button
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * const { applyLayout, getSidebarStyles, getFloatingStyles } = useChatLayout({
62
+ * sidebarWidth: 400,
63
+ * });
64
+ *
65
+ * useEffect(() => {
66
+ * applyLayout(displayMode);
67
+ * }, [displayMode]);
68
+ * ```
69
+ */
70
+ export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
71
+ const mergedConfig = { ...DEFAULT_CONFIG, ...config };
72
+ const { sidebarWidth, zIndex, animationDuration, pushTarget } = mergedConfig;
73
+
74
+ // Store original styles for cleanup
75
+ const originalStylesRef = useRef<{
76
+ marginRight?: string;
77
+ overflowX?: string;
78
+ transition?: string;
79
+ } | null>(null);
80
+
81
+ // Store original styles for fixed elements
82
+ const fixedElementsRef = useRef<FixedElementOriginalStyles[]>([]);
83
+
84
+ // Current mode for cleanup
85
+ const currentModeRef = useRef<ChatDisplayMode>('closed');
86
+
87
+ /**
88
+ * Get the target element to push
89
+ */
90
+ const getTargetElement = useCallback((): HTMLElement | null => {
91
+ if (typeof window === 'undefined') return null;
92
+
93
+ if (pushTarget === 'body') {
94
+ return document.body;
95
+ } else if (pushTarget === 'main') {
96
+ return document.querySelector('main');
97
+ } else {
98
+ return document.querySelector(pushTarget);
99
+ }
100
+ }, [pushTarget]);
101
+
102
+ /**
103
+ * Find all fixed/sticky elements that need right adjustment
104
+ */
105
+ const getFixedElements = useCallback((): HTMLElement[] => {
106
+ if (typeof window === 'undefined') return [];
107
+
108
+ const elements: HTMLElement[] = [];
109
+ const allElements = document.querySelectorAll('*');
110
+
111
+ allElements.forEach((el) => {
112
+ if (!(el instanceof HTMLElement)) return;
113
+ // Skip chat sidebar itself
114
+ if (el.closest('[data-chat-sidebar-panel]')) return;
115
+
116
+ const style = window.getComputedStyle(el);
117
+ const position = style.position;
118
+ const right = style.right;
119
+
120
+ // Check for fixed/sticky elements with right: 0
121
+ if ((position === 'fixed' || position === 'sticky') && right === '0px') {
122
+ elements.push(el);
123
+ }
124
+ });
125
+
126
+ return elements;
127
+ }, []);
128
+
129
+ /**
130
+ * Save original styles before modification
131
+ */
132
+ const saveOriginalStyles = useCallback((element: HTMLElement) => {
133
+ if (!originalStylesRef.current) {
134
+ originalStylesRef.current = {
135
+ marginRight: element.style.marginRight,
136
+ overflowX: element.style.overflowX,
137
+ transition: element.style.transition,
138
+ };
139
+ }
140
+ }, []);
141
+
142
+ /**
143
+ * Restore original styles
144
+ */
145
+ const restoreOriginalStyles = useCallback((element: HTMLElement) => {
146
+ if (originalStylesRef.current) {
147
+ element.style.marginRight = originalStylesRef.current.marginRight || '';
148
+ element.style.overflowX = originalStylesRef.current.overflowX || '';
149
+ element.style.transition = originalStylesRef.current.transition || '';
150
+ element.removeAttribute('data-chat-sidebar');
151
+ originalStylesRef.current = null;
152
+ }
153
+ }, []);
154
+
155
+ /**
156
+ * Adjust fixed elements for sidebar
157
+ */
158
+ const adjustFixedElements = useCallback(
159
+ (open: boolean) => {
160
+ if (open) {
161
+ // Save and adjust fixed elements
162
+ const fixedElements = getFixedElements();
163
+ fixedElementsRef.current = fixedElements.map((el) => ({
164
+ element: el,
165
+ right: el.style.right,
166
+ transition: el.style.transition,
167
+ }));
168
+
169
+ fixedElements.forEach((el) => {
170
+ el.style.transition = `right ${animationDuration}ms ease`;
171
+ el.style.right = `${sidebarWidth}px`;
172
+ });
173
+ } else {
174
+ // Restore fixed elements
175
+ fixedElementsRef.current.forEach(({ element, right, transition }) => {
176
+ element.style.transition = `right ${animationDuration}ms ease`;
177
+ element.style.right = '0px';
178
+
179
+ // Restore original after animation
180
+ setTimeout(() => {
181
+ element.style.right = right;
182
+ element.style.transition = transition;
183
+ }, animationDuration);
184
+ });
185
+ fixedElementsRef.current = [];
186
+ }
187
+ },
188
+ [getFixedElements, sidebarWidth, animationDuration]
189
+ );
190
+
191
+ /**
192
+ * Apply sidebar mode layout
193
+ */
194
+ const applySidebarLayout = useCallback(() => {
195
+ const target = getTargetElement();
196
+ if (!target) return;
197
+
198
+ saveOriginalStyles(target);
199
+
200
+ // Add smooth transition
201
+ target.style.transition = `margin-right ${animationDuration}ms ease`;
202
+ target.style.marginRight = `${sidebarWidth}px`;
203
+ target.style.overflowX = 'hidden';
204
+ target.setAttribute('data-chat-sidebar', 'open');
205
+
206
+ // Adjust fixed elements (header, etc.)
207
+ adjustFixedElements(true);
208
+
209
+ currentModeRef.current = 'sidebar';
210
+ }, [getTargetElement, saveOriginalStyles, sidebarWidth, animationDuration, adjustFixedElements]);
211
+
212
+ /**
213
+ * Apply floating/closed mode layout (reset sidebar push)
214
+ */
215
+ const applyDefaultLayout = useCallback(
216
+ (mode: ChatDisplayMode) => {
217
+ const target = getTargetElement();
218
+ if (!target) return;
219
+
220
+ // Only restore if we were in sidebar mode
221
+ if (currentModeRef.current === 'sidebar') {
222
+ // Add transition for smooth animation
223
+ target.style.transition = `margin-right ${animationDuration}ms ease`;
224
+ target.style.marginRight = '0px';
225
+
226
+ // Restore fixed elements
227
+ adjustFixedElements(false);
228
+
229
+ // Remove styles after animation completes
230
+ setTimeout(() => {
231
+ restoreOriginalStyles(target);
232
+ }, animationDuration);
233
+ }
234
+
235
+ currentModeRef.current = mode;
236
+ },
237
+ [getTargetElement, restoreOriginalStyles, animationDuration, adjustFixedElements]
238
+ );
239
+
240
+ /**
241
+ * Apply layout changes for given mode
242
+ */
243
+ const applyLayout = useCallback(
244
+ (mode: ChatDisplayMode) => {
245
+ if (mode === 'sidebar') {
246
+ applySidebarLayout();
247
+ } else {
248
+ applyDefaultLayout(mode);
249
+ }
250
+ },
251
+ [applySidebarLayout, applyDefaultLayout]
252
+ );
253
+
254
+ /**
255
+ * Reset layout to default (cleanup)
256
+ */
257
+ const resetLayout = useCallback(() => {
258
+ const target = getTargetElement();
259
+ if (target && originalStylesRef.current) {
260
+ restoreOriginalStyles(target);
261
+ }
262
+ // Restore fixed elements immediately on cleanup
263
+ fixedElementsRef.current.forEach(({ element, right, transition }) => {
264
+ element.style.right = right;
265
+ element.style.transition = transition;
266
+ });
267
+ fixedElementsRef.current = [];
268
+ currentModeRef.current = 'closed';
269
+ }, [getTargetElement, restoreOriginalStyles]);
270
+
271
+ /**
272
+ * Get CSS styles for sidebar container
273
+ */
274
+ const getSidebarStyles = useCallback((): React.CSSProperties => {
275
+ return {
276
+ position: 'fixed',
277
+ top: 0,
278
+ right: 0,
279
+ bottom: 0,
280
+ width: `${sidebarWidth}px`,
281
+ zIndex,
282
+ };
283
+ }, [sidebarWidth, zIndex]);
284
+
285
+ /**
286
+ * Get CSS styles for floating container
287
+ */
288
+ const getFloatingStyles = useCallback(
289
+ (position: 'bottom-right' | 'bottom-left'): React.CSSProperties => {
290
+ return {
291
+ position: 'fixed',
292
+ zIndex: zIndex - 50, // Slightly lower than sidebar
293
+ bottom: 56, // Above banner
294
+ ...(position === 'bottom-right' ? { right: 16 } : { left: 16 }),
295
+ };
296
+ },
297
+ [zIndex]
298
+ );
299
+
300
+ /**
301
+ * Get CSS styles for FAB button
302
+ */
303
+ const getFabStyles = useCallback(
304
+ (position: 'bottom-right' | 'bottom-left'): React.CSSProperties => {
305
+ return {
306
+ position: 'fixed',
307
+ zIndex: zIndex - 50,
308
+ bottom: 56,
309
+ ...(position === 'bottom-right' ? { right: 16 } : { left: 16 }),
310
+ };
311
+ },
312
+ [zIndex]
313
+ );
314
+
315
+ // Cleanup on unmount
316
+ useEffect(() => {
317
+ return () => {
318
+ resetLayout();
319
+ };
320
+ }, [resetLayout]);
321
+
322
+ return {
323
+ applyLayout,
324
+ resetLayout,
325
+ getSidebarStyles,
326
+ getFloatingStyles,
327
+ getFabStyles,
328
+ };
329
+ }
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * @djangocfg/mcp-chat
5
+ *
6
+ * React chat components for DjangoCFG documentation assistant.
7
+ * Works with @djangocfg/mcp for semantic search.
8
+ *
9
+ * @example Basic usage (standalone widget)
10
+ * ```tsx
11
+ * import { ChatWidget } from '@djangocfg/mcp-chat';
12
+ *
13
+ * export default function Layout({ children }) {
14
+ * return (
15
+ * <>
16
+ * {children}
17
+ * <ChatWidget apiEndpoint="/api/chat" />
18
+ * </>
19
+ * );
20
+ * }
21
+ * ```
22
+ *
23
+ * @example With provider (for accessing chat from multiple components)
24
+ * ```tsx
25
+ * import { ChatProvider, ChatWidget, useChatContext } from '@djangocfg/mcp-chat';
26
+ *
27
+ * function OpenChatButton() {
28
+ * const { openChat } = useChatContext();
29
+ * return <button onClick={openChat}>Ask AI</button>;
30
+ * }
31
+ *
32
+ * export default function Layout({ children }) {
33
+ * return (
34
+ * <ChatProvider apiEndpoint="/api/chat">
35
+ * {children}
36
+ * <OpenChatButton />
37
+ * <ChatWidget />
38
+ * </ChatProvider>
39
+ * );
40
+ * }
41
+ * ```
42
+ */
43
+
44
+ // Components
45
+ export { ChatWidget, AIChatWidget, ChatPanel, MessageBubble, AIMessageInput } from './components';
46
+ export type {
47
+ ChatWidgetProps,
48
+ AIChatWidgetProps,
49
+ ChatPanelProps,
50
+ MessageBubbleProps,
51
+ AIMessageInputProps,
52
+ } from './components';
53
+
54
+ // Context
55
+ export { AIChatProvider, useAIChatContext, useAIChatContextOptional } from './context';
56
+ export type {
57
+ AIChatContextState,
58
+ AIChatContextActions,
59
+ AIChatContextValue,
60
+ AIChatProviderProps,
61
+ } from './context';
62
+
63
+ // Hooks
64
+ export { useAIChat, useChatLayout } from './hooks';
65
+ export type { ChatLayoutConfig, UseChatLayoutReturn } from './hooks';
66
+
67
+ // Types
68
+ export type {
69
+ AIChatMessage,
70
+ AIChatSource,
71
+ AIMessageRole,
72
+ AIChatApiResponse,
73
+ UseAIChatOptions,
74
+ UseAIChatReturn,
75
+ ChatWidgetConfig,
76
+ } from './types';
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Types for @djangocfg/mcp-chat
3
+ */
4
+
5
+ // Re-export config constants for backwards compatibility
6
+ export { DEFAULT_CHAT_API_ENDPOINT, mcpEndpoints } from './config';
7
+
8
+ /**
9
+ * AI Chat message role
10
+ */
11
+ export type AIMessageRole = 'user' | 'assistant' | 'system';
12
+
13
+ /**
14
+ * AI Chat message
15
+ */
16
+ export interface AIChatMessage {
17
+ id: string;
18
+ role: AIMessageRole;
19
+ content: string;
20
+ timestamp: Date;
21
+ /** Related documentation links */
22
+ sources?: AIChatSource[];
23
+ /** Is message still being generated */
24
+ isStreaming?: boolean;
25
+ }
26
+
27
+ /**
28
+ * AI Documentation source reference
29
+ */
30
+ export interface AIChatSource {
31
+ title: string;
32
+ path: string;
33
+ url?: string;
34
+ section?: string;
35
+ score?: number;
36
+ }
37
+
38
+ /**
39
+ * Chat API response
40
+ */
41
+ export interface ChatApiResponse {
42
+ success: boolean;
43
+ results?: Array<{
44
+ chunk: {
45
+ id: string;
46
+ path: string;
47
+ title: string;
48
+ section?: string;
49
+ content: string;
50
+ url?: string;
51
+ };
52
+ score: number;
53
+ }>;
54
+ answer?: string;
55
+ error?: string;
56
+ }
57
+
58
+ /**
59
+ * Chat display mode
60
+ * - closed: Only FAB button visible
61
+ * - floating: Floating panel (default)
62
+ * - sidebar: Full-height sidebar on the right (desktop only)
63
+ */
64
+ export type ChatDisplayMode = 'closed' | 'floating' | 'sidebar';
65
+
66
+ /**
67
+ * Chat widget configuration
68
+ */
69
+ export interface ChatWidgetConfig {
70
+ /** API endpoint for chat (default: /api/chat) */
71
+ apiEndpoint?: string;
72
+ /** Widget title */
73
+ title?: string;
74
+ /** Placeholder text for input */
75
+ placeholder?: string;
76
+ /** Initial greeting message */
77
+ greeting?: string;
78
+ /** Position on screen */
79
+ position?: 'bottom-right' | 'bottom-left';
80
+ /** Theme variant */
81
+ variant?: 'default' | 'minimal';
82
+ }
83
+
84
+ // =============================================================================
85
+ // AI Chat Types
86
+ // =============================================================================
87
+
88
+ /**
89
+ * AI Chat API response
90
+ */
91
+ export interface AIChatApiResponse {
92
+ success: boolean;
93
+ content?: string;
94
+ sources?: Array<{
95
+ id?: string;
96
+ title: string;
97
+ path: string;
98
+ url?: string;
99
+ section?: string;
100
+ score?: number;
101
+ }>;
102
+ threadId?: string;
103
+ usage?: {
104
+ promptTokens: number;
105
+ completionTokens: number;
106
+ totalTokens: number;
107
+ };
108
+ error?: string;
109
+ }
110
+
111
+ /**
112
+ * useAIChat hook options
113
+ */
114
+ export interface UseAIChatOptions {
115
+ /** API endpoint (default: /api/ai/chat) */
116
+ apiEndpoint?: string;
117
+ /** Initial messages */
118
+ initialMessages?: AIChatMessage[];
119
+ /** Callback on error */
120
+ onError?: (error: Error) => void;
121
+ /** Enable streaming responses (default: true) */
122
+ enableStreaming?: boolean;
123
+ /** Thread ID for conversation (generated if not provided) */
124
+ threadId?: string;
125
+ /** User ID for conversation (generated if not provided) */
126
+ userId?: string;
127
+ }
128
+
129
+ /**
130
+ * useAIChat hook return type
131
+ */
132
+ export interface UseAIChatReturn {
133
+ messages: AIChatMessage[];
134
+ isLoading: boolean;
135
+ error: Error | null;
136
+ threadId: string;
137
+ userId: string;
138
+ sendMessage: (content: string) => Promise<void>;
139
+ clearMessages: () => void;
140
+ stopStreaming: () => void;
141
+ }
@@ -9,3 +9,35 @@ export * from './Breadcrumbs';
9
9
  export * from './AuthDialog';
10
10
  export * from './ContactForm';
11
11
  export * from './Analytics';
12
+
13
+ // MCP Chat (AI-powered documentation assistant)
14
+ export {
15
+ AIChatWidget,
16
+ ChatPanel,
17
+ MessageBubble,
18
+ AIMessageInput,
19
+ AIChatProvider,
20
+ useAIChatContext,
21
+ useAIChatContextOptional,
22
+ useAIChat,
23
+ useChatLayout,
24
+ } from './McpChat';
25
+ export type {
26
+ AIChatWidgetProps,
27
+ ChatPanelProps,
28
+ MessageBubbleProps,
29
+ AIMessageInputProps,
30
+ AIChatContextState,
31
+ AIChatContextActions,
32
+ AIChatContextValue,
33
+ AIChatProviderProps,
34
+ ChatLayoutConfig,
35
+ UseChatLayoutReturn,
36
+ AIChatMessage,
37
+ AIChatSource,
38
+ AIMessageRole,
39
+ AIChatApiResponse,
40
+ UseAIChatOptions,
41
+ UseAIChatReturn,
42
+ ChatWidgetConfig,
43
+ } from './McpChat';
@@ -3,5 +3,4 @@
3
3
  */
4
4
 
5
5
  export * from './logger';
6
- export * from './og-image';
7
6