@djangocfg/ext-knowbase 1.0.0
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/README.md +177 -0
- package/dist/chunk-GNRC54ON.js +4308 -0
- package/dist/hooks.cjs +6003 -0
- package/dist/hooks.d.cts +610 -0
- package/dist/hooks.d.ts +610 -0
- package/dist/hooks.js +1486 -0
- package/dist/index.cjs +4450 -0
- package/dist/index.d.cts +4398 -0
- package/dist/index.d.ts +4398 -0
- package/dist/index.js +1 -0
- package/package.json +81 -0
- package/src/api/generated/ext_knowbase/_utils/fetchers/ext_knowbase__knowbase.ts +2983 -0
- package/src/api/generated/ext_knowbase/_utils/fetchers/index.ts +28 -0
- package/src/api/generated/ext_knowbase/_utils/hooks/ext_knowbase__knowbase.ts +999 -0
- package/src/api/generated/ext_knowbase/_utils/hooks/index.ts +28 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ArchiveItem.schema.ts +33 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ArchiveItemChunk.schema.ts +29 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ArchiveItemChunkDetail.schema.ts +30 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ArchiveItemChunkRequest.schema.ts +22 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ArchiveItemDetail.schema.ts +35 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ArchiveItemRequest.schema.ts +22 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ArchiveProcessingResult.schema.ts +26 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ArchiveSearchRequestRequest.schema.ts +25 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ArchiveSearchResult.schema.ts +24 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ArchiveStatistics.schema.ts +28 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ChatHistory.schema.ts +22 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ChatMessage.schema.ts +27 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ChatQueryRequest.schema.ts +22 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ChatResponse.schema.ts +26 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ChatResponseRequest.schema.ts +26 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ChatSession.schema.ts +29 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ChatSessionCreateRequest.schema.ts +22 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ChatSessionRequest.schema.ts +25 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ChatSource.schema.ts +21 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ChatSourceRequest.schema.ts +21 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/ChunkRevectorizationRequestRequest.schema.ts +20 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/Document.schema.ts +32 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/DocumentArchive.schema.ts +44 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/DocumentArchiveDetail.schema.ts +48 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/DocumentArchiveList.schema.ts +35 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/DocumentArchiveRequest.schema.ts +21 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/DocumentCategory.schema.ts +23 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/DocumentCategoryRequest.schema.ts +21 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/DocumentCreateRequest.schema.ts +22 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/DocumentProcessingStatus.schema.ts +23 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/DocumentRequest.schema.ts +22 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/DocumentStats.schema.ts +25 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PaginatedArchiveItemChunkList.schema.ts +24 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PaginatedArchiveItemList.schema.ts +24 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PaginatedArchiveSearchResultList.schema.ts +24 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PaginatedChatResponseList.schema.ts +24 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PaginatedChatSessionList.schema.ts +24 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PaginatedDocumentArchiveListList.schema.ts +24 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PaginatedDocumentList.schema.ts +24 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PaginatedPublicCategoryList.schema.ts +24 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PaginatedPublicDocumentListList.schema.ts +24 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PatchedArchiveItemChunkRequest.schema.ts +22 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PatchedArchiveItemRequest.schema.ts +22 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PatchedChatResponseRequest.schema.ts +26 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PatchedChatSessionRequest.schema.ts +25 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PatchedDocumentArchiveRequest.schema.ts +21 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PatchedDocumentRequest.schema.ts +22 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PublicCategory.schema.ts +21 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PublicDocument.schema.ts +25 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/PublicDocumentList.schema.ts +24 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/VectorizationResult.schema.ts +24 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/VectorizationStatistics.schema.ts +26 -0
- package/src/api/generated/ext_knowbase/_utils/schemas/index.ts +70 -0
- package/src/api/generated/ext_knowbase/api-instance.ts +131 -0
- package/src/api/generated/ext_knowbase/client.ts +301 -0
- package/src/api/generated/ext_knowbase/enums.ts +241 -0
- package/src/api/generated/ext_knowbase/errors.ts +116 -0
- package/src/api/generated/ext_knowbase/ext_knowbase__knowbase/client.ts +666 -0
- package/src/api/generated/ext_knowbase/ext_knowbase__knowbase/index.ts +2 -0
- package/src/api/generated/ext_knowbase/ext_knowbase__knowbase/models.ts +1120 -0
- package/src/api/generated/ext_knowbase/http.ts +103 -0
- package/src/api/generated/ext_knowbase/index.ts +273 -0
- package/src/api/generated/ext_knowbase/logger.ts +259 -0
- package/src/api/generated/ext_knowbase/retry.ts +175 -0
- package/src/api/generated/ext_knowbase/schema.json +5865 -0
- package/src/api/generated/ext_knowbase/storage.ts +161 -0
- package/src/api/generated/ext_knowbase/validation-events.ts +133 -0
- package/src/api/index.ts +9 -0
- package/src/components/Chat/ChatUIContext.tsx +110 -0
- package/src/components/Chat/ChatWidget.tsx +476 -0
- package/src/components/Chat/README.md +122 -0
- package/src/components/Chat/components/MessageInput.tsx +124 -0
- package/src/components/Chat/components/MessageList.tsx +169 -0
- package/src/components/Chat/components/SessionList.tsx +192 -0
- package/src/components/Chat/components/index.ts +9 -0
- package/src/components/Chat/hooks/index.ts +6 -0
- package/src/components/Chat/hooks/useInfiniteSessions.ts +81 -0
- package/src/components/Chat/index.tsx +45 -0
- package/src/components/Chat/types.ts +81 -0
- package/src/config.ts +20 -0
- package/src/contexts/knowbase/ChatContext.tsx +173 -0
- package/src/contexts/knowbase/DocumentsContext.tsx +306 -0
- package/src/contexts/knowbase/KnowbaseProvider.tsx +47 -0
- package/src/contexts/knowbase/SessionsContext.tsx +175 -0
- package/src/contexts/knowbase/index.ts +63 -0
- package/src/contexts/knowbase/types.ts +69 -0
- package/src/hooks/index.ts +28 -0
- package/src/index.ts +22 -0
- package/src/utils/logger.ts +10 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Input Component
|
|
3
|
+
* Input field for sending chat messages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
9
|
+
import { Button, Textarea } from '@djangocfg/ui-nextjs';
|
|
10
|
+
import { Send, Loader2 } from 'lucide-react';
|
|
11
|
+
import { chatLogger } from '../../../utils/logger';
|
|
12
|
+
import type { MessageInputProps } from '../types';
|
|
13
|
+
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
15
|
+
// Message Input Component
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export const MessageInput: React.FC<MessageInputProps> = ({
|
|
19
|
+
onSend,
|
|
20
|
+
isLoading = false,
|
|
21
|
+
disabled = false,
|
|
22
|
+
placeholder = 'Ask me anything...',
|
|
23
|
+
className = '',
|
|
24
|
+
}) => {
|
|
25
|
+
const [message, setMessage] = useState('');
|
|
26
|
+
const [rows, setRows] = useState(1);
|
|
27
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
28
|
+
|
|
29
|
+
// Auto-resize textarea based on content
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (textareaRef.current) {
|
|
32
|
+
const lineHeight = 24; // approximate line height
|
|
33
|
+
const maxRows = 5;
|
|
34
|
+
const minRows = 1;
|
|
35
|
+
|
|
36
|
+
// If message is empty, reset to min height
|
|
37
|
+
if (!message) {
|
|
38
|
+
textareaRef.current.style.height = 'auto';
|
|
39
|
+
setRows(minRows);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
textareaRef.current.style.height = 'auto';
|
|
44
|
+
const scrollHeight = textareaRef.current.scrollHeight;
|
|
45
|
+
const newRows = Math.max(minRows, Math.min(maxRows, Math.floor(scrollHeight / lineHeight)));
|
|
46
|
+
setRows(newRows);
|
|
47
|
+
}
|
|
48
|
+
}, [message]);
|
|
49
|
+
|
|
50
|
+
const handleSubmit = useCallback(
|
|
51
|
+
async (e?: React.FormEvent) => {
|
|
52
|
+
e?.preventDefault();
|
|
53
|
+
|
|
54
|
+
const trimmedMessage = message.trim();
|
|
55
|
+
if (!trimmedMessage || isLoading || disabled) return;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await onSend(trimmedMessage);
|
|
59
|
+
setMessage('');
|
|
60
|
+
setRows(1);
|
|
61
|
+
|
|
62
|
+
// Reset textarea height
|
|
63
|
+
if (textareaRef.current) {
|
|
64
|
+
textareaRef.current.style.height = 'auto';
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
chatLogger.error('Failed to send message from input:', error);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[message, isLoading, disabled, onSend]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const handleKeyDown = useCallback(
|
|
74
|
+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
75
|
+
// Send on Enter (without Shift)
|
|
76
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
handleSubmit();
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[handleSubmit]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const isDisabled = disabled || isLoading;
|
|
85
|
+
const canSend = message.trim().length > 0 && !isDisabled;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<form onSubmit={handleSubmit} className={`border-t p-4 ${className}`}>
|
|
89
|
+
<div className="flex items-end gap-2">
|
|
90
|
+
<Textarea
|
|
91
|
+
ref={textareaRef}
|
|
92
|
+
value={message}
|
|
93
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
94
|
+
onKeyDown={handleKeyDown}
|
|
95
|
+
placeholder={placeholder}
|
|
96
|
+
disabled={isDisabled}
|
|
97
|
+
rows={rows}
|
|
98
|
+
className="resize-none min-h-[40px] max-h-[120px] transition-all duration-200
|
|
99
|
+
focus:ring-2 focus:ring-primary/20"
|
|
100
|
+
style={{ resize: 'none' }}
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
<Button
|
|
104
|
+
type="submit"
|
|
105
|
+
size="icon"
|
|
106
|
+
disabled={!canSend}
|
|
107
|
+
className="shrink-0 h-10 w-10 transition-all duration-200
|
|
108
|
+
hover:scale-110 active:scale-95 disabled:scale-100"
|
|
109
|
+
>
|
|
110
|
+
{isLoading ? (
|
|
111
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
112
|
+
) : (
|
|
113
|
+
<Send className="h-4 w-4" />
|
|
114
|
+
)}
|
|
115
|
+
</Button>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
119
|
+
Press Enter to send, Shift+Enter for new line
|
|
120
|
+
</p>
|
|
121
|
+
</form>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message List Component
|
|
3
|
+
* Displays chat messages with markdown support and sources
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useEffect, useRef } from 'react';
|
|
9
|
+
import { Card, CardContent, Avatar, AvatarImage, AvatarFallback, Badge, ScrollArea } from '@djangocfg/ui-nextjs';
|
|
10
|
+
import { Bot, User, ExternalLink, Loader2 } from 'lucide-react';
|
|
11
|
+
import { useAuth } from '@djangocfg/api/auth';
|
|
12
|
+
import type { MessageListProps } from '../types';
|
|
13
|
+
import { Enums } from '../types';
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// Message List Component
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export const MessageList: React.FC<MessageListProps> = ({
|
|
20
|
+
messages,
|
|
21
|
+
isLoading = false,
|
|
22
|
+
showSources = true,
|
|
23
|
+
showTimestamps = false,
|
|
24
|
+
autoScroll = true,
|
|
25
|
+
className = '',
|
|
26
|
+
}) => {
|
|
27
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
const { user } = useAuth();
|
|
29
|
+
|
|
30
|
+
// Auto-scroll to bottom on new messages
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (autoScroll && scrollRef.current) {
|
|
33
|
+
const scrollContainer = scrollRef.current.querySelector('[data-radix-scroll-area-viewport]');
|
|
34
|
+
if (scrollContainer) {
|
|
35
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}, [messages, isLoading, autoScroll]);
|
|
39
|
+
|
|
40
|
+
const formatTimestamp = (timestamp: string) => {
|
|
41
|
+
const date = new Date(timestamp);
|
|
42
|
+
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<ScrollArea className={`h-full bg-muted/50 ${className}`} viewportRef={scrollRef}>
|
|
47
|
+
<div className="space-y-4 p-4">
|
|
48
|
+
{messages.length === 0 && !isLoading ? (
|
|
49
|
+
<div className="flex flex-col items-center justify-center h-full text-center py-12">
|
|
50
|
+
<Bot className="h-12 w-12 text-muted-foreground mb-4" />
|
|
51
|
+
<h3 className="text-lg font-semibold text-foreground mb-2">
|
|
52
|
+
Start a Conversation
|
|
53
|
+
</h3>
|
|
54
|
+
<p className="text-sm text-muted-foreground max-w-md">
|
|
55
|
+
Ask me anything about the documentation, features, or get help with your project.
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
) : (
|
|
59
|
+
messages.map((message, index) => {
|
|
60
|
+
const isUser = message.role === Enums.ChatMessageRole.USER;
|
|
61
|
+
const isAssistant = message.role === Enums.ChatMessageRole.ASSISTANT;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
key={message.id}
|
|
66
|
+
className={`flex gap-3 ${isUser ? 'justify-end' : 'justify-start'}
|
|
67
|
+
animate-in fade-in slide-in-from-bottom-2 duration-300`}
|
|
68
|
+
style={{ animationDelay: `${index * 50}ms` }}
|
|
69
|
+
>
|
|
70
|
+
{/* Avatar */}
|
|
71
|
+
{isAssistant && (
|
|
72
|
+
<Avatar className="h-8 w-8 shrink-0">
|
|
73
|
+
<div className="flex h-full w-full items-center justify-center bg-primary text-primary-foreground">
|
|
74
|
+
<Bot className="h-5 w-5" />
|
|
75
|
+
</div>
|
|
76
|
+
</Avatar>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{/* Message Content */}
|
|
80
|
+
<div
|
|
81
|
+
className={`flex flex-col gap-2 flex-1 max-w-[80%] ${
|
|
82
|
+
isUser ? 'items-end' : 'items-start'
|
|
83
|
+
}`}
|
|
84
|
+
>
|
|
85
|
+
{/* Message Bubble */}
|
|
86
|
+
<Card
|
|
87
|
+
className={`${
|
|
88
|
+
isUser
|
|
89
|
+
? 'bg-primary text-primary-foreground'
|
|
90
|
+
: 'bg-muted'
|
|
91
|
+
} transition-all duration-200 hover:shadow-md`}
|
|
92
|
+
>
|
|
93
|
+
<CardContent className="p-3">
|
|
94
|
+
<div className="text-sm whitespace-pre-wrap break-words">
|
|
95
|
+
{message.content}
|
|
96
|
+
</div>
|
|
97
|
+
</CardContent>
|
|
98
|
+
</Card>
|
|
99
|
+
|
|
100
|
+
{/* Timestamp */}
|
|
101
|
+
{showTimestamps && message.created_at && (
|
|
102
|
+
<span className="text-xs text-muted-foreground px-1">
|
|
103
|
+
{formatTimestamp(message.created_at)}
|
|
104
|
+
</span>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{/* Sources */}
|
|
108
|
+
{showSources &&
|
|
109
|
+
isAssistant &&
|
|
110
|
+
message.sources &&
|
|
111
|
+
message.sources.length > 0 && (
|
|
112
|
+
<div className="flex flex-wrap gap-2 px-1 animate-in fade-in slide-in-from-left-2 duration-300 delay-100">
|
|
113
|
+
{message.sources.map((source, idx) => (
|
|
114
|
+
<div key={idx}>
|
|
115
|
+
<Badge
|
|
116
|
+
variant="secondary"
|
|
117
|
+
className="text-xs flex items-center gap-1 cursor-pointer
|
|
118
|
+
hover:bg-secondary/80 hover:scale-105 active:scale-95
|
|
119
|
+
transition-all duration-200 animate-in fade-in zoom-in-95"
|
|
120
|
+
style={{ animationDelay: `${(idx + 1) * 100}ms` }}
|
|
121
|
+
>
|
|
122
|
+
{source.document_title || `Source ${idx + 1}`}
|
|
123
|
+
<ExternalLink className="h-3 w-3" />
|
|
124
|
+
</Badge>
|
|
125
|
+
</div>
|
|
126
|
+
))}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* User Avatar */}
|
|
132
|
+
{isUser && (
|
|
133
|
+
<Avatar className="h-8 w-8 shrink-0">
|
|
134
|
+
{user?.avatar && <AvatarImage src={user.avatar} alt={user.display_username || user.email || 'User'} />}
|
|
135
|
+
<AvatarFallback className="bg-primary/10 text-primary font-semibold">
|
|
136
|
+
{user?.display_username?.charAt(0)?.toUpperCase() ||
|
|
137
|
+
user?.email?.charAt(0)?.toUpperCase() ||
|
|
138
|
+
<User className="h-5 w-5" />}
|
|
139
|
+
</AvatarFallback>
|
|
140
|
+
</Avatar>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
})
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{/* Loading Indicator */}
|
|
148
|
+
{isLoading && (
|
|
149
|
+
<div className="flex gap-3 justify-start animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
150
|
+
<Avatar className="h-8 w-8 shrink-0 animate-pulse">
|
|
151
|
+
<div className="flex h-full w-full items-center justify-center bg-primary text-primary-foreground">
|
|
152
|
+
<Bot className="h-5 w-5 animate-bounce" />
|
|
153
|
+
</div>
|
|
154
|
+
</Avatar>
|
|
155
|
+
<Card className="bg-muted animate-pulse">
|
|
156
|
+
<CardContent className="p-3">
|
|
157
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
158
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
159
|
+
<span className="animate-pulse">Thinking...</span>
|
|
160
|
+
</div>
|
|
161
|
+
</CardContent>
|
|
162
|
+
</Card>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</ScrollArea>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session List Component
|
|
3
|
+
* Drawer with list of chat sessions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useCallback, useRef, useEffect } from 'react';
|
|
9
|
+
import {
|
|
10
|
+
Sheet,
|
|
11
|
+
SheetContent,
|
|
12
|
+
SheetHeader,
|
|
13
|
+
SheetTitle,
|
|
14
|
+
SheetDescription,
|
|
15
|
+
Button,
|
|
16
|
+
ScrollArea,
|
|
17
|
+
Badge,
|
|
18
|
+
} from '@djangocfg/ui-nextjs';
|
|
19
|
+
import { MessageSquare, Clock, Archive, Trash2, Loader2 } from 'lucide-react';
|
|
20
|
+
import { useKnowbaseSessionsContext } from '../../../contexts/knowbase';
|
|
21
|
+
import { useInfiniteSessions } from '../hooks/useInfiniteSessions';
|
|
22
|
+
import type { SessionListProps } from '../types';
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Session List Component
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export const SessionList: React.FC<SessionListProps> = ({
|
|
29
|
+
isOpen,
|
|
30
|
+
onClose,
|
|
31
|
+
onSelectSession,
|
|
32
|
+
className = '',
|
|
33
|
+
}) => {
|
|
34
|
+
const { deleteSession, archiveSession } = useKnowbaseSessionsContext();
|
|
35
|
+
const {
|
|
36
|
+
sessions,
|
|
37
|
+
isLoading,
|
|
38
|
+
isLoadingMore,
|
|
39
|
+
hasMore,
|
|
40
|
+
loadMore,
|
|
41
|
+
} = useInfiniteSessions();
|
|
42
|
+
|
|
43
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
|
|
45
|
+
// Handle scroll to load more
|
|
46
|
+
const handleScroll = useCallback(() => {
|
|
47
|
+
if (!scrollRef.current || isLoadingMore || !hasMore) return;
|
|
48
|
+
|
|
49
|
+
const scrollContainer = scrollRef.current.querySelector('[data-radix-scroll-area-viewport]');
|
|
50
|
+
if (!scrollContainer) return;
|
|
51
|
+
|
|
52
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
|
53
|
+
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
|
|
54
|
+
|
|
55
|
+
// Load more when scrolled 80% down
|
|
56
|
+
if (scrollPercentage > 0.8) {
|
|
57
|
+
loadMore();
|
|
58
|
+
}
|
|
59
|
+
}, [hasMore, isLoadingMore, loadMore]);
|
|
60
|
+
|
|
61
|
+
// Attach scroll listener
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const scrollContainer = scrollRef.current?.querySelector('[data-radix-scroll-area-viewport]');
|
|
64
|
+
if (!scrollContainer) return;
|
|
65
|
+
|
|
66
|
+
scrollContainer.addEventListener('scroll', handleScroll);
|
|
67
|
+
return () => scrollContainer.removeEventListener('scroll', handleScroll);
|
|
68
|
+
}, [handleScroll]);
|
|
69
|
+
|
|
70
|
+
const formatDate = (dateString: string) => {
|
|
71
|
+
const date = new Date(dateString);
|
|
72
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Sheet open={isOpen} onOpenChange={onClose}>
|
|
77
|
+
<SheetContent side="left" className={`w-[400px] sm:w-[540px] ${className}`}>
|
|
78
|
+
<SheetHeader>
|
|
79
|
+
<SheetTitle>Chat Sessions</SheetTitle>
|
|
80
|
+
<SheetDescription>
|
|
81
|
+
View and manage your chat history
|
|
82
|
+
</SheetDescription>
|
|
83
|
+
</SheetHeader>
|
|
84
|
+
|
|
85
|
+
<ScrollArea className="h-[calc(100vh-120px)] mt-6" viewportRef={scrollRef}>
|
|
86
|
+
{isLoading ? (
|
|
87
|
+
<div className="flex items-center justify-center py-12">
|
|
88
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
89
|
+
</div>
|
|
90
|
+
) : !sessions || sessions.length === 0 ? (
|
|
91
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
92
|
+
<MessageSquare className="h-12 w-12 text-muted-foreground mb-4" />
|
|
93
|
+
<h3 className="text-lg font-semibold text-foreground mb-2">
|
|
94
|
+
No Sessions Yet
|
|
95
|
+
</h3>
|
|
96
|
+
<p className="text-sm text-muted-foreground max-w-md">
|
|
97
|
+
Start a new conversation to create your first session.
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
) : (
|
|
101
|
+
<div className="space-y-2">
|
|
102
|
+
{sessions.map((session, index) => (
|
|
103
|
+
<div
|
|
104
|
+
key={session.id}
|
|
105
|
+
className="group relative flex items-start gap-3 p-4 border rounded-sm
|
|
106
|
+
hover:bg-muted/50 hover:border-primary/50 hover:shadow-md
|
|
107
|
+
transition-all duration-200 cursor-pointer
|
|
108
|
+
animate-in fade-in slide-in-from-left-2"
|
|
109
|
+
style={{ animationDelay: `${index * 50}ms` }}
|
|
110
|
+
onClick={() => {
|
|
111
|
+
onSelectSession(session.id);
|
|
112
|
+
onClose();
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{/* Session Icon */}
|
|
116
|
+
<div className="shrink-0 mt-1">
|
|
117
|
+
<MessageSquare className="h-5 w-5 text-muted-foreground" />
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Session Info */}
|
|
121
|
+
<div className="flex-1 min-w-0">
|
|
122
|
+
<div className="flex items-start justify-between gap-2 mb-1">
|
|
123
|
+
<h4 className="font-medium text-sm truncate">
|
|
124
|
+
{session.title || 'Untitled Session'}
|
|
125
|
+
</h4>
|
|
126
|
+
{session.is_active && (
|
|
127
|
+
<Badge variant="default" className="shrink-0 text-xs">
|
|
128
|
+
Active
|
|
129
|
+
</Badge>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
134
|
+
<Clock className="h-3 w-3" />
|
|
135
|
+
<span>{formatDate(session.created_at)}</span>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Actions */}
|
|
140
|
+
<div className="shrink-0 flex items-center gap-1 opacity-0 group-hover:opacity-100
|
|
141
|
+
transition-all duration-200 group-hover:translate-x-0 translate-x-2">
|
|
142
|
+
<Button
|
|
143
|
+
variant="ghost"
|
|
144
|
+
size="sm"
|
|
145
|
+
className="h-8 w-8 p-0 hover:scale-110 active:scale-95 transition-transform duration-200"
|
|
146
|
+
onClick={(e) => {
|
|
147
|
+
e.stopPropagation();
|
|
148
|
+
archiveSession(session.id, {});
|
|
149
|
+
}}
|
|
150
|
+
title="Archive"
|
|
151
|
+
>
|
|
152
|
+
<Archive className="h-4 w-4" />
|
|
153
|
+
</Button>
|
|
154
|
+
<Button
|
|
155
|
+
variant="ghost"
|
|
156
|
+
size="sm"
|
|
157
|
+
className="h-8 w-8 p-0 text-destructive hover:text-destructive
|
|
158
|
+
hover:scale-110 active:scale-95 transition-transform duration-200"
|
|
159
|
+
onClick={(e) => {
|
|
160
|
+
e.stopPropagation();
|
|
161
|
+
deleteSession(session.id);
|
|
162
|
+
}}
|
|
163
|
+
title="Delete"
|
|
164
|
+
>
|
|
165
|
+
<Trash2 className="h-4 w-4" />
|
|
166
|
+
</Button>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
))}
|
|
170
|
+
|
|
171
|
+
{/* Loading more indicator */}
|
|
172
|
+
{isLoadingMore && (
|
|
173
|
+
<div className="flex items-center justify-center py-6">
|
|
174
|
+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground mr-2" />
|
|
175
|
+
<span className="text-sm text-muted-foreground">Loading more...</span>
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{/* No more sessions indicator */}
|
|
180
|
+
{!hasMore && sessions.length > 0 && (
|
|
181
|
+
<div className="text-center py-4">
|
|
182
|
+
<span className="text-xs text-muted-foreground">No more sessions</span>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</ScrollArea>
|
|
188
|
+
</SheetContent>
|
|
189
|
+
</Sheet>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for infinite scroll chat sessions
|
|
3
|
+
* Uses SWR Infinite for pagination
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import useSWRInfinite from 'swr/infinite';
|
|
7
|
+
import { apiKnowbase, ExtKnowbaseTypes, ExtKnowbaseFetchers } from '@djangocfg/api';
|
|
8
|
+
|
|
9
|
+
type PaginatedChatSessionList = ExtKnowbaseTypes.PaginatedChatSessionList;
|
|
10
|
+
type ChatSession = ExtKnowbaseTypes.ChatSession;
|
|
11
|
+
|
|
12
|
+
const PAGE_SIZE = 20;
|
|
13
|
+
|
|
14
|
+
export function useInfiniteSessions() {
|
|
15
|
+
const getKey = (pageIndex: number, previousPageData: PaginatedChatSessionList | null) => {
|
|
16
|
+
// Reached the end
|
|
17
|
+
if (previousPageData && !previousPageData.has_next) return null;
|
|
18
|
+
|
|
19
|
+
// First page, no previous data
|
|
20
|
+
if (pageIndex === 0) return ['cfg-knowbase-admin-sessions-infinite', 1, PAGE_SIZE];
|
|
21
|
+
|
|
22
|
+
// Add the page number to the SWR key
|
|
23
|
+
return ['cfg-knowbase-admin-sessions-infinite', pageIndex + 1, PAGE_SIZE];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const fetcher = async ([, page, pageSize]: [string, number, number]) => {
|
|
27
|
+
return ExtKnowbaseFetchers.getKnowbaseAdminSessionsList(
|
|
28
|
+
{ page, page_size: pageSize },
|
|
29
|
+
apiKnowbase
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
data,
|
|
35
|
+
error,
|
|
36
|
+
isLoading,
|
|
37
|
+
isValidating,
|
|
38
|
+
size,
|
|
39
|
+
setSize,
|
|
40
|
+
mutate,
|
|
41
|
+
} = useSWRInfinite<PaginatedChatSessionList>(getKey, fetcher, {
|
|
42
|
+
revalidateFirstPage: false,
|
|
43
|
+
parallel: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Flatten all pages into single array
|
|
47
|
+
const sessions: ChatSession[] = data ? data.flatMap((page) => page.results) : [];
|
|
48
|
+
|
|
49
|
+
// Check if there are more pages
|
|
50
|
+
const hasMore = data && data[data.length - 1]?.has_next;
|
|
51
|
+
|
|
52
|
+
// Total count from last page
|
|
53
|
+
const totalCount = data && data[data.length - 1]?.count;
|
|
54
|
+
|
|
55
|
+
// Loading more state
|
|
56
|
+
const isLoadingMore = isValidating && data && typeof data[size - 1] !== 'undefined';
|
|
57
|
+
|
|
58
|
+
// Function to load next page
|
|
59
|
+
const loadMore = () => {
|
|
60
|
+
if (hasMore && !isLoadingMore) {
|
|
61
|
+
setSize(size + 1);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Refresh all pages
|
|
66
|
+
const refresh = async () => {
|
|
67
|
+
await mutate();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
sessions,
|
|
72
|
+
isLoading,
|
|
73
|
+
isLoadingMore: isLoadingMore || false,
|
|
74
|
+
error,
|
|
75
|
+
hasMore: hasMore || false,
|
|
76
|
+
totalCount: totalCount || 0,
|
|
77
|
+
loadMore,
|
|
78
|
+
refresh,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Knowledge Chat Module
|
|
4
|
+
* Complete RAG-powered chat widget with context providers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { KnowbaseChatProvider, KnowbaseSessionsProvider } from '../../contexts/knowbase';
|
|
11
|
+
import { ChatUIProvider } from './ChatUIContext';
|
|
12
|
+
import { ChatWidget } from './ChatWidget';
|
|
13
|
+
import type { ChatWidgetProps } from './types';
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// Main Component with Providers
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export const KnowledgeChat: React.FC<ChatWidgetProps> = (props) => {
|
|
20
|
+
return (
|
|
21
|
+
<KnowbaseChatProvider>
|
|
22
|
+
<KnowbaseSessionsProvider>
|
|
23
|
+
<ChatUIProvider initialState={{ isOpen: false }}>
|
|
24
|
+
<ChatWidget {...props} />
|
|
25
|
+
</ChatUIProvider>
|
|
26
|
+
</KnowbaseSessionsProvider>
|
|
27
|
+
</KnowbaseChatProvider>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Named Exports
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export { ChatWidget } from './ChatWidget';
|
|
36
|
+
export { ChatUIProvider, useChatUI } from './ChatUIContext';
|
|
37
|
+
export { MessageList, MessageInput, SessionList } from './components';
|
|
38
|
+
export * from './types';
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// Default Export
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export default KnowledgeChat;
|
|
45
|
+
|