@djangocfg/layouts 2.0.6 → 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 +14 -13
- package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +3 -22
- package/src/layouts/SupportLayout/components/MessageList.tsx +1 -1
- package/src/layouts/SupportLayout/components/TicketList.tsx +1 -1
- package/src/snippets/Chat/components/MessageList.tsx +1 -1
- package/src/snippets/Chat/components/SessionList.tsx +1 -1
- 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,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;
|