@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,119 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect } from 'react';
4
+ import { Button } from '@djangocfg/ui';
5
+ import { X, PanelRightClose, Bot } from 'lucide-react';
6
+ import { ChatMessages } from './ChatMessages';
7
+ import { AIMessageInput } from './MessageInput';
8
+ import { useChatLayout } from '../hooks/useChatLayout';
9
+ import type { AIChatMessage, ChatDisplayMode } from '../types';
10
+
11
+ const SIDEBAR_WIDTH = 400;
12
+
13
+ export interface ChatSidebarProps {
14
+ messages: AIChatMessage[];
15
+ isLoading: boolean;
16
+ onSendMessage: (content: string) => void;
17
+ onClose?: () => void;
18
+ onModeChange?: (mode: ChatDisplayMode) => void;
19
+ onStopStreaming?: () => void;
20
+ title?: string;
21
+ placeholder?: string;
22
+ greeting?: string;
23
+ }
24
+
25
+ export const ChatSidebar = React.memo<ChatSidebarProps>(
26
+ ({
27
+ messages,
28
+ isLoading,
29
+ onSendMessage,
30
+ onClose,
31
+ onModeChange,
32
+ onStopStreaming,
33
+ title = 'DjangoCFG Assistant',
34
+ placeholder,
35
+ greeting,
36
+ }) => {
37
+ // Use the layout hook for content pushing
38
+ const { applyLayout, getSidebarStyles } = useChatLayout({
39
+ sidebarWidth: SIDEBAR_WIDTH,
40
+ });
41
+
42
+ // Apply sidebar layout on mount, reset on unmount
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ useEffect(() => {
45
+ applyLayout('sidebar');
46
+ return () => {
47
+ applyLayout('closed');
48
+ };
49
+ }, []);
50
+
51
+ const sidebarStyles = getSidebarStyles();
52
+
53
+ return (
54
+ <div
55
+ className="flex flex-col bg-background border-l border-border"
56
+ style={sidebarStyles}
57
+ data-chat-sidebar-panel
58
+ >
59
+ {/* Header - uses CSS variable for navbar height consistency */}
60
+ <div
61
+ className="flex items-center justify-between px-4 border-b border-border bg-muted/30"
62
+ style={{ height: 'var(--nextra-navbar-height, 64px)', minHeight: 'var(--nextra-navbar-height, 64px)' }}
63
+ >
64
+ <div className="flex items-center gap-2">
65
+ <div
66
+ className="rounded-full bg-primary/10 flex items-center justify-center"
67
+ style={{ width: '32px', height: '32px' }}
68
+ >
69
+ <Bot className="h-4 w-4 text-primary" />
70
+ </div>
71
+ <div>
72
+ <h3 className="font-semibold text-sm">{title}</h3>
73
+ <p className="text-xs text-muted-foreground">Documentation assistant</p>
74
+ </div>
75
+ </div>
76
+ <div className="flex gap-1">
77
+ {onModeChange && (
78
+ <Button
79
+ variant="ghost"
80
+ size="icon"
81
+ className="h-8 w-8"
82
+ onClick={() => onModeChange('floating')}
83
+ title="Switch to floating mode"
84
+ >
85
+ <PanelRightClose className="h-4 w-4" />
86
+ </Button>
87
+ )}
88
+ {onClose && (
89
+ <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onClose} title="Close chat">
90
+ <X className="h-4 w-4" />
91
+ </Button>
92
+ )}
93
+ </div>
94
+ </div>
95
+
96
+ {/* Messages */}
97
+ <div className="flex-1 overflow-hidden">
98
+ <ChatMessages
99
+ messages={messages}
100
+ isLoading={isLoading}
101
+ greeting={greeting}
102
+ onStopStreaming={onStopStreaming}
103
+ isCompact={false}
104
+ largeGreetingIcon
105
+ greetingIcon="message"
106
+ greetingTitle="How can I help?"
107
+ />
108
+ </div>
109
+
110
+ {/* Input */}
111
+ <div className="p-4 border-t border-border bg-muted/30">
112
+ <AIMessageInput onSend={onSendMessage} isLoading={isLoading} placeholder={placeholder} />
113
+ </div>
114
+ </div>
115
+ );
116
+ }
117
+ );
118
+
119
+ ChatSidebar.displayName = 'ChatSidebar';
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Button, Portal } from '@djangocfg/ui';
5
+ import { MessageCircle } from 'lucide-react';
6
+ import { ChatPanel } from './ChatPanel';
7
+ import { ChatSidebar } from './ChatSidebar';
8
+ import { useChatContext, useChatContextOptional, ChatProvider } from '../context';
9
+ import { useChatLayout } from '../hooks/useChatLayout';
10
+ import type { ChatWidgetConfig } from '../types';
11
+
12
+ export interface ChatWidgetProps extends ChatWidgetConfig {
13
+ /** Custom class name for the container */
14
+ className?: string;
15
+ }
16
+
17
+ /**
18
+ * Internal chat widget that uses context
19
+ */
20
+ const ChatWidgetInternal: React.FC<{ className?: string }> = ({ className }) => {
21
+ const {
22
+ messages,
23
+ isLoading,
24
+ isMinimized,
25
+ config,
26
+ displayMode,
27
+ isMobile,
28
+ sendMessage,
29
+ openChat,
30
+ closeChat,
31
+ toggleMinimize,
32
+ setDisplayMode,
33
+ } = useChatContext();
34
+
35
+ // Use layout hook for consistent positioning
36
+ const { getFabStyles, getFloatingStyles } = useChatLayout();
37
+
38
+ const position = config.position || 'bottom-right';
39
+ const fabStyles = getFabStyles(position);
40
+ const floatingStyles = getFloatingStyles(position);
41
+
42
+ // Mode: closed - just show FAB
43
+ if (displayMode === 'closed') {
44
+ return (
45
+ <Portal>
46
+ <div style={fabStyles} className={className || ''}>
47
+ <Button
48
+ onClick={openChat}
49
+ className="rounded-full shadow-lg hover:shadow-xl transition-shadow"
50
+ style={{ width: '56px', height: '56px' }}
51
+ >
52
+ <MessageCircle className="h-6 w-6" />
53
+ </Button>
54
+ </div>
55
+ </Portal>
56
+ );
57
+ }
58
+
59
+ // Mode: sidebar - full-height panel on the right (desktop only)
60
+ if (displayMode === 'sidebar') {
61
+ return (
62
+ <Portal>
63
+ <ChatSidebar
64
+ messages={messages}
65
+ isLoading={isLoading}
66
+ onSendMessage={sendMessage}
67
+ onClose={closeChat}
68
+ onModeChange={setDisplayMode}
69
+ title={config.title}
70
+ placeholder={config.placeholder}
71
+ greeting={config.greeting}
72
+ />
73
+ </Portal>
74
+ );
75
+ }
76
+
77
+ // Mode: floating - floating panel
78
+ return (
79
+ <Portal>
80
+ <div style={floatingStyles} className={className || ''}>
81
+ <ChatPanel
82
+ messages={messages}
83
+ isLoading={isLoading}
84
+ onSendMessage={sendMessage}
85
+ onClose={closeChat}
86
+ onMinimize={toggleMinimize}
87
+ onModeChange={setDisplayMode}
88
+ isMinimized={isMinimized}
89
+ isMobile={isMobile}
90
+ title={config.title}
91
+ placeholder={config.placeholder}
92
+ greeting={config.greeting}
93
+ />
94
+ </div>
95
+ </Portal>
96
+ );
97
+ };
98
+
99
+ /**
100
+ * ChatWidget component
101
+ *
102
+ * Can be used in two ways:
103
+ * 1. Standalone (wraps itself in ChatProvider)
104
+ * 2. Inside a ChatProvider (uses context directly)
105
+ */
106
+ export const ChatWidget: React.FC<ChatWidgetProps> = ({
107
+ apiEndpoint = '/api/chat',
108
+ title = 'DjangoCFG Assistant',
109
+ placeholder = 'Ask about DjangoCFG...',
110
+ greeting = "Hi! I'm your DjangoCFG documentation assistant. Ask me anything about configuration, features, or how to use the library.",
111
+ position = 'bottom-right',
112
+ variant = 'default',
113
+ className,
114
+ }) => {
115
+ // Check if we're inside a ChatProvider
116
+ const existingContext = useChatContextOptional();
117
+
118
+ // If already in context, use internal widget directly
119
+ if (existingContext) {
120
+ return <ChatWidgetInternal className={className} />;
121
+ }
122
+
123
+ // Otherwise, wrap in provider
124
+ return (
125
+ <ChatProvider
126
+ apiEndpoint={apiEndpoint}
127
+ config={{ title, placeholder, greeting, position, variant }}
128
+ >
129
+ <ChatWidgetInternal className={className} />
130
+ </ChatProvider>
131
+ );
132
+ };
133
+
134
+ ChatWidget.displayName = 'ChatWidget';
@@ -0,0 +1,125 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Card, CardContent, Badge, MarkdownMessage } from '@djangocfg/ui';
5
+ import { User, Bot, ExternalLink, Loader2 } from 'lucide-react';
6
+ import type { AIChatMessage } from '../types';
7
+
8
+ export interface MessageBubbleProps {
9
+ message: AIChatMessage;
10
+ isCompact?: boolean;
11
+ }
12
+
13
+ function formatTime(date: Date): string {
14
+ return date.toLocaleTimeString('en-US', {
15
+ hour: '2-digit',
16
+ minute: '2-digit',
17
+ });
18
+ }
19
+
20
+ export const MessageBubble = React.memo<MessageBubbleProps>(
21
+ ({ message, isCompact = false }) => {
22
+ const isUser = message.role === 'user';
23
+ const isAssistant = message.role === 'assistant';
24
+
25
+ return (
26
+ <div
27
+ className={`flex gap-3 animate-in fade-in slide-in-from-bottom-2 duration-300 ${
28
+ isUser ? 'flex-row-reverse' : ''
29
+ }`}
30
+ >
31
+ {/* Avatar */}
32
+ <div
33
+ className={`flex-shrink-0 rounded-full flex items-center justify-center ${
34
+ isUser
35
+ ? 'bg-primary text-primary-foreground'
36
+ : 'bg-muted text-muted-foreground'
37
+ }`}
38
+ style={{
39
+ width: isCompact ? '28px' : '36px',
40
+ height: isCompact ? '28px' : '36px',
41
+ }}
42
+ >
43
+ {isUser ? (
44
+ <User className={isCompact ? 'h-3.5 w-3.5' : 'h-4 w-4'} />
45
+ ) : (
46
+ <Bot className={isCompact ? 'h-3.5 w-3.5' : 'h-4 w-4'} />
47
+ )}
48
+ </div>
49
+
50
+ {/* Message Content */}
51
+ <div className={`flex-1 min-w-0 ${isUser ? 'max-w-[80%] ml-auto' : 'max-w-[85%]'}`}>
52
+ {/* Header */}
53
+ <div className={`flex items-baseline gap-2 mb-1 ${isUser ? 'justify-end' : ''}`}>
54
+ <span className={`font-medium ${isCompact ? 'text-xs' : 'text-sm'}`}>
55
+ {isUser ? 'You' : 'DjangoCFG Assistant'}
56
+ </span>
57
+ <span className="text-xs text-muted-foreground">
58
+ {formatTime(message.timestamp)}
59
+ </span>
60
+ </div>
61
+
62
+ {/* Message Bubble */}
63
+ <Card
64
+ className={`transition-all duration-200 ${
65
+ isUser
66
+ ? 'bg-primary text-primary-foreground ml-auto'
67
+ : 'bg-muted'
68
+ }`}
69
+ >
70
+ <CardContent className={isCompact ? 'p-2' : 'p-3'}>
71
+ {/* Message Text */}
72
+ <div className={`${isCompact ? 'text-xs' : 'text-sm'}`}>
73
+ {isUser ? (
74
+ <span className="whitespace-pre-wrap">{message.content}</span>
75
+ ) : message.isStreaming ? (
76
+ // During streaming - show plain text to avoid parsing errors
77
+ <span className="whitespace-pre-wrap">
78
+ {message.content}
79
+ <Loader2 className="inline-block ml-1 h-3 w-3 animate-spin" />
80
+ </span>
81
+ ) : (
82
+ // Streaming complete - render with markdown formatting
83
+ <MarkdownMessage
84
+ content={message.content}
85
+ isUser={isUser}
86
+ className="prose-sm"
87
+ />
88
+ )}
89
+ </div>
90
+
91
+ {/* Sources */}
92
+ {isAssistant && message.sources && message.sources.length > 0 && (
93
+ <div className="mt-3 pt-2 border-t border-border/50">
94
+ <p className="text-xs text-muted-foreground mb-1.5">Related docs:</p>
95
+ <div className="flex flex-wrap gap-1.5">
96
+ {message.sources.slice(0, 3).map((source, idx) => (
97
+ <a
98
+ key={idx}
99
+ href={source.url || source.path}
100
+ target="_blank"
101
+ rel="noopener noreferrer"
102
+ className="inline-block"
103
+ >
104
+ <Badge
105
+ variant="outline"
106
+ className="text-xs gap-1 hover:bg-primary/10 transition-colors cursor-pointer"
107
+ >
108
+ {source.title}
109
+ {source.section && ` - ${source.section}`}
110
+ <ExternalLink className="h-2.5 w-2.5" />
111
+ </Badge>
112
+ </a>
113
+ ))}
114
+ </div>
115
+ </div>
116
+ )}
117
+ </CardContent>
118
+ </Card>
119
+ </div>
120
+ </div>
121
+ );
122
+ }
123
+ );
124
+
125
+ MessageBubble.displayName = 'MessageBubble';
@@ -0,0 +1,139 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useRef, useCallback, useEffect } from 'react';
4
+ import { Button } from '@djangocfg/ui';
5
+ import { Send, Loader2 } from 'lucide-react';
6
+
7
+ export interface AIMessageInputProps {
8
+ onSend: (message: string) => void;
9
+ disabled?: boolean;
10
+ isLoading?: boolean;
11
+ placeholder?: string;
12
+ maxRows?: number;
13
+ }
14
+
15
+ export const AIMessageInput = React.memo<AIMessageInputProps>(
16
+ ({
17
+ onSend,
18
+ disabled = false,
19
+ isLoading = false,
20
+ placeholder = 'Ask about DjangoCFG...',
21
+ maxRows = 5,
22
+ }) => {
23
+ const [value, setValue] = useState('');
24
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
25
+
26
+ // Auto-resize textarea
27
+ const adjustHeight = useCallback(() => {
28
+ const textarea = textareaRef.current;
29
+ if (!textarea) return;
30
+
31
+ // Reset height to auto to get the correct scrollHeight
32
+ textarea.style.height = 'auto';
33
+
34
+ // Calculate line height and max height
35
+ const lineHeight = 24; // ~1.5rem
36
+ const minHeight = 44; // Single line height with padding
37
+ const maxHeight = lineHeight * maxRows + 20; // padding
38
+
39
+ // Set new height
40
+ const newHeight = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);
41
+ textarea.style.height = `${newHeight}px`;
42
+ }, [maxRows]);
43
+
44
+ // Adjust height when value changes
45
+ useEffect(() => {
46
+ adjustHeight();
47
+ }, [value, adjustHeight]);
48
+
49
+ const handleSubmit = useCallback(
50
+ (e?: React.FormEvent) => {
51
+ e?.preventDefault();
52
+
53
+ const trimmed = value.trim();
54
+ if (!trimmed || disabled || isLoading) return;
55
+
56
+ onSend(trimmed);
57
+ setValue('');
58
+
59
+ // Reset height after sending
60
+ if (textareaRef.current) {
61
+ textareaRef.current.style.height = 'auto';
62
+ }
63
+ textareaRef.current?.focus();
64
+ },
65
+ [value, disabled, isLoading, onSend]
66
+ );
67
+
68
+ const handleKeyDown = useCallback(
69
+ (e: React.KeyboardEvent) => {
70
+ if (e.key === 'Enter' && !e.shiftKey) {
71
+ e.preventDefault();
72
+ handleSubmit();
73
+ }
74
+ },
75
+ [handleSubmit]
76
+ );
77
+
78
+ const canSend = value.trim().length > 0 && !disabled && !isLoading;
79
+
80
+ return (
81
+ <form onSubmit={handleSubmit} className="w-full">
82
+ <div
83
+ 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"
84
+ style={{ minHeight: '44px' }}
85
+ >
86
+ {/* Textarea */}
87
+ <textarea
88
+ ref={textareaRef}
89
+ value={value}
90
+ onChange={(e) => setValue(e.target.value)}
91
+ onKeyDown={handleKeyDown}
92
+ placeholder={placeholder}
93
+ disabled={disabled || isLoading}
94
+ rows={1}
95
+ 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"
96
+ style={{
97
+ minHeight: '44px',
98
+ maxHeight: `${24 * maxRows + 20}px`,
99
+ lineHeight: '1.5rem',
100
+ }}
101
+ autoComplete="off"
102
+ />
103
+
104
+ {/* Send Button - positioned inside, bottom right */}
105
+ <div
106
+ className="absolute flex items-center justify-center"
107
+ style={{
108
+ right: '6px',
109
+ bottom: '6px',
110
+ }}
111
+ >
112
+ <Button
113
+ type="submit"
114
+ size="icon"
115
+ disabled={!canSend}
116
+ className="h-8 w-8 rounded-full transition-all"
117
+ style={{
118
+ opacity: canSend ? 1 : 0.5,
119
+ }}
120
+ >
121
+ {isLoading ? (
122
+ <Loader2 className="h-4 w-4 animate-spin" />
123
+ ) : (
124
+ <Send className="h-4 w-4" />
125
+ )}
126
+ </Button>
127
+ </div>
128
+ </div>
129
+
130
+ {/* Helper text */}
131
+ <p className="mt-1.5 text-xs text-muted-foreground text-center">
132
+ Press Enter to send, Shift+Enter for new line
133
+ </p>
134
+ </form>
135
+ );
136
+ }
137
+ );
138
+
139
+ AIMessageInput.displayName = 'AIMessageInput';
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+
3
+ export { ChatWidget } from './ChatWidget';
4
+ export type { ChatWidgetProps } from './ChatWidget';
5
+
6
+ export { AIChatWidget } from './AIChatWidget';
7
+ export type { AIChatWidgetProps } from './AIChatWidget';
8
+
9
+ export { ChatPanel } from './ChatPanel';
10
+ export type { ChatPanelProps } from './ChatPanel';
11
+
12
+ export { ChatMessages } from './ChatMessages';
13
+ export type { ChatMessagesProps, ChatMessagesHandle } from './ChatMessages';
14
+
15
+ export { MessageBubble } from './MessageBubble';
16
+ export type { MessageBubbleProps } from './MessageBubble';
17
+
18
+ export { AIMessageInput } from './MessageInput';
19
+ export type { AIMessageInputProps } from './MessageInput';
20
+
21
+ export { ChatSidebar } from './ChatSidebar';
22
+ export type { ChatSidebarProps } from './ChatSidebar';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * McpChat configuration
3
+ * Environment-aware configuration for MCP server endpoints
4
+ */
5
+
6
+ // Hosts
7
+ const PROD_HOST = 'https://mcp.djangocfg.com';
8
+ const DEV_HOST = 'http://localhost:3002';
9
+
10
+ // Current host based on environment
11
+ const HOST = process.env.NODE_ENV === 'development' ? DEV_HOST : PROD_HOST;
12
+
13
+ /**
14
+ * MCP Server endpoints
15
+ */
16
+ export const mcpEndpoints = {
17
+ /** Base URL */
18
+ baseUrl: HOST,
19
+ /** Chat API endpoint */
20
+ chat: `${HOST}/api/chat`,
21
+ /** Search API endpoint */
22
+ search: `${HOST}/api/search`,
23
+ /** Conversations API endpoint */
24
+ conversations: `${HOST}/api/conversations`,
25
+ /** Health check endpoint */
26
+ health: `${HOST}/health`,
27
+ /** MCP protocol endpoint (Streamable HTTP) */
28
+ mcp: `${HOST}/mcp`,
29
+ /** SSE endpoint for legacy clients */
30
+ sse: `${HOST}/mcp/sse`,
31
+ };
32
+
33
+ // Export defaults for backwards compatibility
34
+ export const DEFAULT_CHAT_API_ENDPOINT = PROD_HOST + '/api/chat';
35
+ export const DEFAULT_MCP_BASE_URL = PROD_HOST;