@djangocfg/layouts 2.0.5 → 2.0.7
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 +19 -13
- package/src/contexts/LeadsContext.tsx +156 -0
- package/src/contexts/NewsletterContext.tsx +263 -0
- package/src/contexts/SupportContext.tsx +256 -0
- package/src/contexts/index.ts +59 -0
- package/src/contexts/knowbase/ChatContext.tsx +174 -0
- package/src/contexts/knowbase/DocumentsContext.tsx +304 -0
- package/src/contexts/knowbase/SessionsContext.tsx +174 -0
- package/src/contexts/knowbase/index.ts +61 -0
- package/src/contexts/payments/BalancesContext.tsx +65 -0
- package/src/contexts/payments/CurrenciesContext.tsx +66 -0
- package/src/contexts/payments/OverviewContext.tsx +174 -0
- package/src/contexts/payments/PaymentsContext.tsx +132 -0
- package/src/contexts/payments/README.md +201 -0
- package/src/contexts/payments/RootPaymentsContext.tsx +68 -0
- package/src/contexts/payments/index.ts +50 -0
- package/src/index.ts +4 -1
- package/src/layouts/AppLayout/AppLayout.tsx +20 -10
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +1 -1
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +1 -1
- package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +3 -22
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +1 -1
- package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +1 -1
- package/src/layouts/PaymentsLayout/views/transactions/components/TransactionsList.tsx +1 -1
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +1 -1
- package/src/layouts/SupportLayout/SupportLayout.tsx +1 -1
- package/src/layouts/SupportLayout/components/MessageList.tsx +1 -1
- package/src/layouts/SupportLayout/components/TicketCard.tsx +1 -1
- package/src/layouts/SupportLayout/components/TicketList.tsx +1 -1
- package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +1 -1
- package/src/layouts/SupportLayout/index.ts +2 -0
- package/src/layouts/SupportLayout/types.ts +2 -4
- package/src/snippets/Chat/ChatWidget.tsx +1 -1
- package/src/snippets/Chat/components/MessageList.tsx +1 -1
- package/src/snippets/Chat/components/SessionList.tsx +2 -2
- package/src/snippets/Chat/index.tsx +1 -1
- package/src/snippets/Chat/types.ts +7 -5
- package/src/snippets/ContactForm/ContactForm.tsx +20 -8
- package/src/snippets/McpChat/components/AIChatWidget.tsx +268 -0
- package/src/snippets/McpChat/components/ChatMessages.tsx +151 -0
- package/src/snippets/McpChat/components/ChatPanel.tsx +126 -0
- package/src/snippets/McpChat/components/ChatSidebar.tsx +119 -0
- package/src/snippets/McpChat/components/ChatWidget.tsx +134 -0
- package/src/snippets/McpChat/components/MessageBubble.tsx +125 -0
- package/src/snippets/McpChat/components/MessageInput.tsx +139 -0
- package/src/snippets/McpChat/components/index.ts +22 -0
- package/src/snippets/McpChat/config.ts +35 -0
- package/src/snippets/McpChat/context/AIChatContext.tsx +245 -0
- package/src/snippets/McpChat/context/ChatContext.tsx +350 -0
- package/src/snippets/McpChat/context/index.ts +7 -0
- package/src/snippets/McpChat/hooks/index.ts +5 -0
- package/src/snippets/McpChat/hooks/useAIChat.ts +487 -0
- package/src/snippets/McpChat/hooks/useChatLayout.ts +329 -0
- package/src/snippets/McpChat/index.ts +76 -0
- package/src/snippets/McpChat/types.ts +141 -0
- package/src/snippets/index.ts +32 -0
- package/src/utils/index.ts +0 -1
- package/src/utils/og-image.ts +0 -169
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useRef, useEffect, useCallback, useImperativeHandle, forwardRef } from 'react';
|
|
4
|
+
import { ScrollArea, Button, type ScrollAreaHandle } from '@djangocfg/ui';
|
|
5
|
+
import { Bot, MessageSquare, StopCircle } from 'lucide-react';
|
|
6
|
+
import { MessageBubble } from './MessageBubble';
|
|
7
|
+
import type { AIChatMessage } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ChatMessages imperative handle
|
|
11
|
+
*/
|
|
12
|
+
export interface ChatMessagesHandle {
|
|
13
|
+
scrollToBottom: (instant?: boolean) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ChatMessagesProps {
|
|
17
|
+
/** Messages to display */
|
|
18
|
+
messages: AIChatMessage[];
|
|
19
|
+
/** Whether loading/streaming is in progress */
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
/** Greeting to show when no messages */
|
|
22
|
+
greeting?: string;
|
|
23
|
+
/** Callback to stop streaming */
|
|
24
|
+
onStopStreaming?: () => void;
|
|
25
|
+
/** Use compact layout (smaller bubbles) */
|
|
26
|
+
isCompact?: boolean;
|
|
27
|
+
/** Use larger icon for greeting (sidebar mode) */
|
|
28
|
+
largeGreetingIcon?: boolean;
|
|
29
|
+
/** Custom greeting icon */
|
|
30
|
+
greetingIcon?: 'bot' | 'message';
|
|
31
|
+
/** Custom greeting title */
|
|
32
|
+
greetingTitle?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ChatMessages - Shared component for displaying chat messages
|
|
37
|
+
*
|
|
38
|
+
* Handles:
|
|
39
|
+
* - Messages list rendering
|
|
40
|
+
* - Greeting display when empty
|
|
41
|
+
* - Loading indicator with stop button
|
|
42
|
+
* - Auto-scroll on new messages
|
|
43
|
+
* - Initial scroll on history load
|
|
44
|
+
*/
|
|
45
|
+
export const ChatMessages = forwardRef<ChatMessagesHandle, ChatMessagesProps>(
|
|
46
|
+
(
|
|
47
|
+
{
|
|
48
|
+
messages,
|
|
49
|
+
isLoading,
|
|
50
|
+
greeting,
|
|
51
|
+
onStopStreaming,
|
|
52
|
+
isCompact = false,
|
|
53
|
+
largeGreetingIcon = false,
|
|
54
|
+
greetingIcon = 'bot',
|
|
55
|
+
greetingTitle,
|
|
56
|
+
},
|
|
57
|
+
ref
|
|
58
|
+
) => {
|
|
59
|
+
const scrollAreaRef = useRef<ScrollAreaHandle>(null);
|
|
60
|
+
const hasScrolledOnLoad = useRef(false);
|
|
61
|
+
|
|
62
|
+
// Scroll to bottom helper
|
|
63
|
+
const scrollToBottom = useCallback((instant = false) => {
|
|
64
|
+
if (scrollAreaRef.current) {
|
|
65
|
+
scrollAreaRef.current.scrollToBottom(instant ? 'instant' : 'smooth');
|
|
66
|
+
}
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
// Expose scrollToBottom via ref
|
|
70
|
+
useImperativeHandle(ref, () => ({
|
|
71
|
+
scrollToBottom,
|
|
72
|
+
}), [scrollToBottom]);
|
|
73
|
+
|
|
74
|
+
// Initial scroll when history loads (instant, no animation)
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!isLoading && messages.length > 0 && !hasScrolledOnLoad.current) {
|
|
77
|
+
requestAnimationFrame(() => {
|
|
78
|
+
scrollToBottom(true);
|
|
79
|
+
hasScrolledOnLoad.current = true;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}, [isLoading, messages.length, scrollToBottom]);
|
|
83
|
+
|
|
84
|
+
// Scroll to bottom on new messages (smooth animation)
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (hasScrolledOnLoad.current && messages.length > 0) {
|
|
87
|
+
scrollToBottom(false);
|
|
88
|
+
}
|
|
89
|
+
}, [messages, scrollToBottom]);
|
|
90
|
+
|
|
91
|
+
const GreetingIcon = greetingIcon === 'message' ? MessageSquare : Bot;
|
|
92
|
+
const iconSize = largeGreetingIcon ? { container: '64px', icon: 'h-8 w-8' } : { container: '48px', icon: 'h-6 w-6' };
|
|
93
|
+
const padding = largeGreetingIcon ? 'py-12' : 'py-8';
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<ScrollArea ref={scrollAreaRef} className="h-full">
|
|
97
|
+
<div className={`${isCompact ? 'p-3' : 'p-4'} space-y-4`}>
|
|
98
|
+
{/* Greeting */}
|
|
99
|
+
{messages.length === 0 && greeting && (
|
|
100
|
+
<div className={`text-center ${padding}`}>
|
|
101
|
+
<div
|
|
102
|
+
className="mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center"
|
|
103
|
+
style={{ width: iconSize.container, height: iconSize.container }}
|
|
104
|
+
>
|
|
105
|
+
<GreetingIcon className={`${iconSize.icon} text-primary`} />
|
|
106
|
+
</div>
|
|
107
|
+
{greetingTitle && (
|
|
108
|
+
<h4 className="font-medium mb-2">{greetingTitle}</h4>
|
|
109
|
+
)}
|
|
110
|
+
<p className={`text-sm text-muted-foreground ${largeGreetingIcon ? 'max-w-[300px]' : 'max-w-[280px]'} mx-auto`}>
|
|
111
|
+
{greeting}
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{/* Messages */}
|
|
117
|
+
{messages.map((message) => (
|
|
118
|
+
<MessageBubble key={message.id} message={message} isCompact={isCompact} />
|
|
119
|
+
))}
|
|
120
|
+
|
|
121
|
+
{/* Loading indicator with stop button */}
|
|
122
|
+
{isLoading && messages.length > 0 && (
|
|
123
|
+
<div className="flex items-center justify-between text-muted-foreground text-sm">
|
|
124
|
+
<div className="flex items-center gap-2">
|
|
125
|
+
<div className="flex gap-1">
|
|
126
|
+
<span className="animate-bounce" style={{ animationDelay: '0ms' }}>.</span>
|
|
127
|
+
<span className="animate-bounce" style={{ animationDelay: '150ms' }}>.</span>
|
|
128
|
+
<span className="animate-bounce" style={{ animationDelay: '300ms' }}>.</span>
|
|
129
|
+
</div>
|
|
130
|
+
<span>Generating response...</span>
|
|
131
|
+
</div>
|
|
132
|
+
{onStopStreaming && (
|
|
133
|
+
<Button
|
|
134
|
+
variant="ghost"
|
|
135
|
+
size="sm"
|
|
136
|
+
onClick={onStopStreaming}
|
|
137
|
+
className="h-6 px-2 text-xs"
|
|
138
|
+
>
|
|
139
|
+
<StopCircle className="h-3 w-3 mr-1" />
|
|
140
|
+
Stop
|
|
141
|
+
</Button>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
</ScrollArea>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
ChatMessages.displayName = 'ChatMessages';
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Card, CardHeader, CardContent, CardFooter, Button } from '@djangocfg/ui';
|
|
5
|
+
import { X, Minimize2, PanelRight, Bot } from 'lucide-react';
|
|
6
|
+
import type { ChatDisplayMode, AIChatMessage } from '../types';
|
|
7
|
+
import { ChatMessages } from './ChatMessages';
|
|
8
|
+
import { AIMessageInput } from './MessageInput';
|
|
9
|
+
|
|
10
|
+
export interface ChatPanelProps {
|
|
11
|
+
messages: AIChatMessage[];
|
|
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
|
+
({
|
|
27
|
+
messages,
|
|
28
|
+
isLoading,
|
|
29
|
+
onSendMessage,
|
|
30
|
+
onClose,
|
|
31
|
+
onMinimize,
|
|
32
|
+
onModeChange,
|
|
33
|
+
onStopStreaming,
|
|
34
|
+
isMinimized = false,
|
|
35
|
+
isMobile = false,
|
|
36
|
+
title = 'DjangoCFG Assistant',
|
|
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
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Card
|
|
54
|
+
className="flex flex-col shadow-2xl border-border/50"
|
|
55
|
+
style={{
|
|
56
|
+
width: '380px',
|
|
57
|
+
height: '520px',
|
|
58
|
+
maxHeight: 'calc(100vh - 100px)',
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{/* Header */}
|
|
62
|
+
<CardHeader className="flex flex-row items-center justify-between p-3 border-b">
|
|
63
|
+
<div className="flex items-center gap-2">
|
|
64
|
+
<div
|
|
65
|
+
className="rounded-full bg-primary/10 flex items-center justify-center"
|
|
66
|
+
style={{ width: '32px', height: '32px' }}
|
|
67
|
+
>
|
|
68
|
+
<Bot className="h-4 w-4 text-primary" />
|
|
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>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="flex gap-1">
|
|
76
|
+
{/* Sidebar mode button - only on desktop */}
|
|
77
|
+
{onModeChange && !isMobile && (
|
|
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
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
</CardHeader>
|
|
100
|
+
|
|
101
|
+
{/* Messages */}
|
|
102
|
+
<CardContent className="flex-1 p-0 overflow-hidden">
|
|
103
|
+
<ChatMessages
|
|
104
|
+
messages={messages}
|
|
105
|
+
isLoading={isLoading}
|
|
106
|
+
greeting={greeting}
|
|
107
|
+
onStopStreaming={onStopStreaming}
|
|
108
|
+
isCompact
|
|
109
|
+
greetingIcon="bot"
|
|
110
|
+
/>
|
|
111
|
+
</CardContent>
|
|
112
|
+
|
|
113
|
+
{/* Input */}
|
|
114
|
+
<CardFooter className="p-3 border-t">
|
|
115
|
+
<AIMessageInput
|
|
116
|
+
onSend={onSendMessage}
|
|
117
|
+
isLoading={isLoading}
|
|
118
|
+
placeholder={placeholder}
|
|
119
|
+
/>
|
|
120
|
+
</CardFooter>
|
|
121
|
+
</Card>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
ChatPanel.displayName = 'ChatPanel';
|
|
@@ -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';
|