@djangocfg/ext-knowbase 1.0.21 → 1.0.23
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/dist/config.cjs +12 -3
- package/dist/config.js +12 -3
- package/dist/hooks.cjs +209 -37
- package/dist/hooks.js +210 -38
- package/dist/i18n.cjs +164 -0
- package/dist/i18n.d.cts +72 -0
- package/dist/i18n.d.ts +72 -0
- package/dist/i18n.js +136 -0
- package/dist/index.cjs +12 -3
- package/dist/index.js +12 -3
- package/package.json +20 -11
- package/src/components/Chat/ChatWidget.tsx +30 -16
- package/src/components/Chat/components/MessageInput.tsx +15 -8
- package/src/components/Chat/components/MessageList.tsx +16 -7
- package/src/components/Chat/components/SessionList.tsx +29 -14
- package/src/i18n/index.ts +26 -0
- package/src/i18n/locales/en.ts +36 -0
- package/src/i18n/locales/ko.ts +36 -0
- package/src/i18n/locales/ru.ts +36 -0
- package/src/i18n/types.ts +59 -0
- package/src/i18n/useKnowbaseT.ts +60 -0
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { List, Maximize2, MessageSquare, Minimize2, Plus, X } from 'lucide-react';
|
|
9
9
|
import { usePathname } from 'next/navigation';
|
|
10
|
-
import React, { useCallback, useEffect, useState } from 'react';
|
|
10
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
11
11
|
import { createPortal } from 'react-dom';
|
|
12
12
|
|
|
13
|
+
import { useKnowbaseT } from '../../i18n';
|
|
13
14
|
import {
|
|
14
15
|
Button, Card, CardContent, CardHeader,
|
|
15
16
|
} from '@djangocfg/ui-nextjs';
|
|
@@ -37,6 +38,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
37
38
|
onToggle,
|
|
38
39
|
onMessage,
|
|
39
40
|
}) => {
|
|
41
|
+
const kt = useKnowbaseT();
|
|
40
42
|
const { sendQuery, getChatHistory } = useKnowbaseChatContext();
|
|
41
43
|
const { createSession, getSession } = useKnowbaseSessionsContext();
|
|
42
44
|
const {
|
|
@@ -48,9 +50,21 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
48
50
|
toggleTimestamps,
|
|
49
51
|
} = useChatUI();
|
|
50
52
|
|
|
53
|
+
const labels = useMemo(() => ({
|
|
54
|
+
title: kt('chat.title'),
|
|
55
|
+
titleShort: kt('chat.titleShort'),
|
|
56
|
+
placeholder: kt('chat.placeholder'),
|
|
57
|
+
openChat: kt('chat.openChat'),
|
|
58
|
+
sessions: kt('actions.sessions'),
|
|
59
|
+
newChat: kt('sessions.newChat'),
|
|
60
|
+
collapse: kt('actions.collapse'),
|
|
61
|
+
expand: kt('actions.expand'),
|
|
62
|
+
close: kt('actions.close'),
|
|
63
|
+
}), [kt]);
|
|
64
|
+
|
|
51
65
|
const pathname = usePathname();
|
|
52
66
|
const isMobile = useIsMobile();
|
|
53
|
-
|
|
67
|
+
|
|
54
68
|
const [isLoading, setIsLoading] = useState(false);
|
|
55
69
|
const [showSessions, setShowSessions] = useState(false);
|
|
56
70
|
const [mounted, setMounted] = useState(false);
|
|
@@ -233,7 +247,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
233
247
|
<div className="flex items-center justify-between p-4 border-b bg-background shadow-sm">
|
|
234
248
|
<div className="flex items-center gap-2">
|
|
235
249
|
<MessageSquare className="h-5 w-5 text-primary" />
|
|
236
|
-
<h2 className="font-semibold text-foreground">
|
|
250
|
+
<h2 className="font-semibold text-foreground">{labels.title}</h2>
|
|
237
251
|
</div>
|
|
238
252
|
|
|
239
253
|
<div className="flex items-center gap-2">
|
|
@@ -242,7 +256,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
242
256
|
size="sm"
|
|
243
257
|
onClick={() => setShowSessions(true)}
|
|
244
258
|
className="text-muted-foreground hover:text-foreground"
|
|
245
|
-
title=
|
|
259
|
+
title={labels.sessions}
|
|
246
260
|
>
|
|
247
261
|
<List className="h-5 w-5" />
|
|
248
262
|
</Button>
|
|
@@ -252,7 +266,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
252
266
|
size="sm"
|
|
253
267
|
onClick={handleNewChat}
|
|
254
268
|
className="text-muted-foreground hover:text-foreground"
|
|
255
|
-
title=
|
|
269
|
+
title={labels.newChat}
|
|
256
270
|
>
|
|
257
271
|
<Plus className="h-5 w-5" />
|
|
258
272
|
</Button>
|
|
@@ -282,7 +296,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
282
296
|
<MessageInput
|
|
283
297
|
onSend={handleSendMessage}
|
|
284
298
|
isLoading={isLoading}
|
|
285
|
-
placeholder=
|
|
299
|
+
placeholder={labels.placeholder}
|
|
286
300
|
/>
|
|
287
301
|
</div>
|
|
288
302
|
</div>
|
|
@@ -342,7 +356,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
342
356
|
<CardHeader className="flex-row items-center justify-between space-y-0 pb-3 border-b">
|
|
343
357
|
<div className="flex items-center gap-2">
|
|
344
358
|
<MessageSquare className="h-5 w-5 text-primary" />
|
|
345
|
-
<h3 className="font-semibold text-foreground">
|
|
359
|
+
<h3 className="font-semibold text-foreground">{labels.titleShort}</h3>
|
|
346
360
|
</div>
|
|
347
361
|
|
|
348
362
|
<div className="flex items-center gap-1">
|
|
@@ -351,7 +365,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
351
365
|
size="sm"
|
|
352
366
|
onClick={() => setShowSessions(true)}
|
|
353
367
|
className="h-8 w-8 p-0"
|
|
354
|
-
title=
|
|
368
|
+
title={labels.sessions}
|
|
355
369
|
>
|
|
356
370
|
<List className="h-4 w-4" />
|
|
357
371
|
</Button>
|
|
@@ -361,7 +375,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
361
375
|
size="sm"
|
|
362
376
|
onClick={handleNewChat}
|
|
363
377
|
className="h-8 w-8 p-0"
|
|
364
|
-
title=
|
|
378
|
+
title={labels.newChat}
|
|
365
379
|
>
|
|
366
380
|
<Plus className="h-4 w-4" />
|
|
367
381
|
</Button>
|
|
@@ -371,7 +385,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
371
385
|
size="sm"
|
|
372
386
|
onClick={uiState.isExpanded ? collapseChat : expandChat}
|
|
373
387
|
className="h-8 w-8 p-0"
|
|
374
|
-
title={uiState.isExpanded ?
|
|
388
|
+
title={uiState.isExpanded ? labels.collapse : labels.expand}
|
|
375
389
|
>
|
|
376
390
|
{uiState.isExpanded ? (
|
|
377
391
|
<Minimize2 className="h-4 w-4" />
|
|
@@ -385,7 +399,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
385
399
|
size="sm"
|
|
386
400
|
onClick={toggleChat}
|
|
387
401
|
className="h-8 w-8 p-0"
|
|
388
|
-
title=
|
|
402
|
+
title={labels.close}
|
|
389
403
|
>
|
|
390
404
|
<X className="h-4 w-4" />
|
|
391
405
|
</Button>
|
|
@@ -406,7 +420,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
406
420
|
<MessageInput
|
|
407
421
|
onSend={handleSendMessage}
|
|
408
422
|
isLoading={isLoading}
|
|
409
|
-
placeholder=
|
|
423
|
+
placeholder={labels.placeholder}
|
|
410
424
|
/>
|
|
411
425
|
</CardContent>
|
|
412
426
|
</Card>
|
|
@@ -439,8 +453,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
439
453
|
`}</style>
|
|
440
454
|
<Button
|
|
441
455
|
onClick={toggleChat}
|
|
442
|
-
className="w-full h-full rounded-full shadow-2xl
|
|
443
|
-
hover:scale-110 hover:rotate-12 active:scale-95
|
|
456
|
+
className="w-full h-full rounded-full shadow-2xl
|
|
457
|
+
hover:scale-110 hover:rotate-12 active:scale-95
|
|
444
458
|
transition-all duration-300 ease-out
|
|
445
459
|
flex items-center justify-center
|
|
446
460
|
group"
|
|
@@ -449,8 +463,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
449
463
|
borderRadius: '50%',
|
|
450
464
|
padding: 0,
|
|
451
465
|
}}
|
|
452
|
-
title=
|
|
453
|
-
aria-label=
|
|
466
|
+
title={labels.openChat}
|
|
467
|
+
aria-label={labels.openChat}
|
|
454
468
|
>
|
|
455
469
|
<MessageSquare
|
|
456
470
|
className="h-7 w-7 text-primary-foreground group-hover:scale-110 transition-transform duration-300"
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
8
|
import { Loader2, Send } from 'lucide-react';
|
|
9
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
9
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
10
10
|
|
|
11
|
+
import { useKnowbaseT } from '../../../i18n';
|
|
11
12
|
import { Button, Textarea } from '@djangocfg/ui-nextjs';
|
|
12
13
|
|
|
13
14
|
import { chatLogger } from '../../../utils/logger';
|
|
@@ -22,13 +23,19 @@ export const MessageInput: React.FC<MessageInputProps> = ({
|
|
|
22
23
|
onSend,
|
|
23
24
|
isLoading = false,
|
|
24
25
|
disabled = false,
|
|
25
|
-
placeholder
|
|
26
|
+
placeholder,
|
|
26
27
|
className = '',
|
|
27
28
|
}) => {
|
|
29
|
+
const kt = useKnowbaseT();
|
|
28
30
|
const [message, setMessage] = useState('');
|
|
29
31
|
const [rows, setRows] = useState(1);
|
|
30
32
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
31
33
|
|
|
34
|
+
const labels = useMemo(() => ({
|
|
35
|
+
placeholder: placeholder || kt('chat.placeholder'),
|
|
36
|
+
enterToSend: kt('chat.enterToSend'),
|
|
37
|
+
}), [kt, placeholder]);
|
|
38
|
+
|
|
32
39
|
// Auto-resize textarea based on content
|
|
33
40
|
useEffect(() => {
|
|
34
41
|
if (textareaRef.current) {
|
|
@@ -95,19 +102,19 @@ export const MessageInput: React.FC<MessageInputProps> = ({
|
|
|
95
102
|
value={message}
|
|
96
103
|
onChange={(e) => setMessage(e.target.value)}
|
|
97
104
|
onKeyDown={handleKeyDown}
|
|
98
|
-
placeholder={placeholder}
|
|
105
|
+
placeholder={labels.placeholder}
|
|
99
106
|
disabled={isDisabled}
|
|
100
107
|
rows={rows}
|
|
101
|
-
className="resize-none min-h-[40px] max-h-[120px] transition-all duration-200
|
|
108
|
+
className="resize-none min-h-[40px] max-h-[120px] transition-all duration-200
|
|
102
109
|
focus:ring-2 focus:ring-primary/20"
|
|
103
110
|
style={{ resize: 'none' }}
|
|
104
111
|
/>
|
|
105
|
-
|
|
112
|
+
|
|
106
113
|
<Button
|
|
107
114
|
type="submit"
|
|
108
115
|
size="icon"
|
|
109
116
|
disabled={!canSend}
|
|
110
|
-
className="shrink-0 h-10 w-10 transition-all duration-200
|
|
117
|
+
className="shrink-0 h-10 w-10 transition-all duration-200
|
|
111
118
|
hover:scale-110 active:scale-95 disabled:scale-100"
|
|
112
119
|
>
|
|
113
120
|
{isLoading ? (
|
|
@@ -117,9 +124,9 @@ export const MessageInput: React.FC<MessageInputProps> = ({
|
|
|
117
124
|
)}
|
|
118
125
|
</Button>
|
|
119
126
|
</div>
|
|
120
|
-
|
|
127
|
+
|
|
121
128
|
<p className="text-xs text-muted-foreground mt-2">
|
|
122
|
-
|
|
129
|
+
{labels.enterToSend}
|
|
123
130
|
</p>
|
|
124
131
|
</form>
|
|
125
132
|
);
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { Bot, ExternalLink, Loader2, User } from 'lucide-react';
|
|
9
9
|
import moment from 'moment';
|
|
10
|
-
import React, { useEffect, useRef } from 'react';
|
|
10
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
11
11
|
|
|
12
12
|
import { useAuth } from '@djangocfg/api/auth';
|
|
13
|
+
import { useKnowbaseT } from '../../../i18n';
|
|
13
14
|
import {
|
|
14
15
|
Avatar, AvatarFallback, AvatarImage, Badge, Card, CardContent, ScrollArea
|
|
15
16
|
} from '@djangocfg/ui-nextjs';
|
|
@@ -29,9 +30,17 @@ export const MessageList: React.FC<MessageListProps> = ({
|
|
|
29
30
|
autoScroll = true,
|
|
30
31
|
className = '',
|
|
31
32
|
}) => {
|
|
33
|
+
const kt = useKnowbaseT();
|
|
32
34
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
33
35
|
const { user } = useAuth();
|
|
34
36
|
|
|
37
|
+
const labels = useMemo(() => ({
|
|
38
|
+
startConversation: kt('chat.startConversation'),
|
|
39
|
+
startConversationDescription: kt('chat.startConversationDescription'),
|
|
40
|
+
source: kt('chat.source'),
|
|
41
|
+
thinking: kt('chat.thinking'),
|
|
42
|
+
}), [kt]);
|
|
43
|
+
|
|
35
44
|
// Auto-scroll to bottom on new messages
|
|
36
45
|
useEffect(() => {
|
|
37
46
|
if (autoScroll && scrollRef.current) {
|
|
@@ -58,10 +67,10 @@ export const MessageList: React.FC<MessageListProps> = ({
|
|
|
58
67
|
<div className="flex flex-col items-center justify-center h-full text-center py-12">
|
|
59
68
|
<Bot className="h-12 w-12 text-muted-foreground mb-4" />
|
|
60
69
|
<h3 className="text-lg font-semibold text-foreground mb-2">
|
|
61
|
-
|
|
70
|
+
{labels.startConversation}
|
|
62
71
|
</h3>
|
|
63
72
|
<p className="text-sm text-muted-foreground max-w-md">
|
|
64
|
-
|
|
73
|
+
{labels.startConversationDescription}
|
|
65
74
|
</p>
|
|
66
75
|
</div>
|
|
67
76
|
) : (
|
|
@@ -123,12 +132,12 @@ export const MessageList: React.FC<MessageListProps> = ({
|
|
|
123
132
|
<div key={idx}>
|
|
124
133
|
<Badge
|
|
125
134
|
variant="secondary"
|
|
126
|
-
className="text-xs flex items-center gap-1 cursor-pointer
|
|
127
|
-
hover:bg-secondary/80 hover:scale-105 active:scale-95
|
|
135
|
+
className="text-xs flex items-center gap-1 cursor-pointer
|
|
136
|
+
hover:bg-secondary/80 hover:scale-105 active:scale-95
|
|
128
137
|
transition-all duration-200 animate-in fade-in zoom-in-95"
|
|
129
138
|
style={{ animationDelay: `${(idx + 1) * 100}ms` }}
|
|
130
139
|
>
|
|
131
|
-
{source.document_title ||
|
|
140
|
+
{source.document_title || labels.source.replace('{index}', String(idx + 1))}
|
|
132
141
|
<ExternalLink className="h-3 w-3" />
|
|
133
142
|
</Badge>
|
|
134
143
|
</div>
|
|
@@ -163,7 +172,7 @@ export const MessageList: React.FC<MessageListProps> = ({
|
|
|
163
172
|
<CardContent className="p-3">
|
|
164
173
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
165
174
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
166
|
-
<span className="animate-pulse">
|
|
175
|
+
<span className="animate-pulse">{labels.thinking}</span>
|
|
167
176
|
</div>
|
|
168
177
|
</CardContent>
|
|
169
178
|
</Card>
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import { Archive, Clock, Loader2, MessageSquare, Trash2 } from 'lucide-react';
|
|
9
9
|
import moment from 'moment';
|
|
10
|
-
import React, { useCallback, useEffect, useRef } from 'react';
|
|
10
|
+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
11
11
|
|
|
12
|
+
import { useKnowbaseT } from '../../../i18n';
|
|
12
13
|
import {
|
|
13
14
|
Badge, Button, ScrollArea, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle
|
|
14
15
|
} from '@djangocfg/ui-nextjs';
|
|
@@ -28,6 +29,7 @@ export const SessionList: React.FC<SessionListProps> = ({
|
|
|
28
29
|
onSelectSession,
|
|
29
30
|
className = '',
|
|
30
31
|
}) => {
|
|
32
|
+
const kt = useKnowbaseT();
|
|
31
33
|
const { deleteSession, archiveSession } = useKnowbaseSessionsContext();
|
|
32
34
|
const {
|
|
33
35
|
sessions,
|
|
@@ -36,7 +38,20 @@ export const SessionList: React.FC<SessionListProps> = ({
|
|
|
36
38
|
hasMore,
|
|
37
39
|
loadMore,
|
|
38
40
|
} = useInfiniteSessions();
|
|
39
|
-
|
|
41
|
+
|
|
42
|
+
const labels = useMemo(() => ({
|
|
43
|
+
title: kt('sessions.title'),
|
|
44
|
+
description: kt('sessions.description'),
|
|
45
|
+
noSessions: kt('sessions.noSessions'),
|
|
46
|
+
noSessionsDescription: kt('sessions.noSessionsDescription'),
|
|
47
|
+
untitled: kt('sessions.untitled'),
|
|
48
|
+
active: kt('sessions.active'),
|
|
49
|
+
archive: kt('sessions.archive'),
|
|
50
|
+
delete: kt('sessions.delete'),
|
|
51
|
+
loadingMore: kt('sessions.loadingMore'),
|
|
52
|
+
noMore: kt('sessions.noMore'),
|
|
53
|
+
}), [kt]);
|
|
54
|
+
|
|
40
55
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
41
56
|
|
|
42
57
|
// Handle scroll to load more
|
|
@@ -72,9 +87,9 @@ export const SessionList: React.FC<SessionListProps> = ({
|
|
|
72
87
|
<Sheet open={isOpen} onOpenChange={onClose}>
|
|
73
88
|
<SheetContent side="left" className={`w-[400px] sm:w-[540px] ${className}`}>
|
|
74
89
|
<SheetHeader>
|
|
75
|
-
<SheetTitle>
|
|
90
|
+
<SheetTitle>{labels.title}</SheetTitle>
|
|
76
91
|
<SheetDescription>
|
|
77
|
-
|
|
92
|
+
{labels.description}
|
|
78
93
|
</SheetDescription>
|
|
79
94
|
</SheetHeader>
|
|
80
95
|
|
|
@@ -87,10 +102,10 @@ export const SessionList: React.FC<SessionListProps> = ({
|
|
|
87
102
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
88
103
|
<MessageSquare className="h-12 w-12 text-muted-foreground mb-4" />
|
|
89
104
|
<h3 className="text-lg font-semibold text-foreground mb-2">
|
|
90
|
-
|
|
105
|
+
{labels.noSessions}
|
|
91
106
|
</h3>
|
|
92
107
|
<p className="text-sm text-muted-foreground max-w-md">
|
|
93
|
-
|
|
108
|
+
{labels.noSessionsDescription}
|
|
94
109
|
</p>
|
|
95
110
|
</div>
|
|
96
111
|
) : (
|
|
@@ -117,11 +132,11 @@ export const SessionList: React.FC<SessionListProps> = ({
|
|
|
117
132
|
<div className="flex-1 min-w-0">
|
|
118
133
|
<div className="flex items-start justify-between gap-2 mb-1">
|
|
119
134
|
<h4 className="font-medium text-sm truncate">
|
|
120
|
-
{session.title ||
|
|
135
|
+
{session.title || labels.untitled}
|
|
121
136
|
</h4>
|
|
122
137
|
{session.is_active && (
|
|
123
138
|
<Badge variant="default" className="shrink-0 text-xs">
|
|
124
|
-
|
|
139
|
+
{labels.active}
|
|
125
140
|
</Badge>
|
|
126
141
|
)}
|
|
127
142
|
</div>
|
|
@@ -143,20 +158,20 @@ export const SessionList: React.FC<SessionListProps> = ({
|
|
|
143
158
|
e.stopPropagation();
|
|
144
159
|
archiveSession(session.id, {});
|
|
145
160
|
}}
|
|
146
|
-
title=
|
|
161
|
+
title={labels.archive}
|
|
147
162
|
>
|
|
148
163
|
<Archive className="h-4 w-4" />
|
|
149
164
|
</Button>
|
|
150
165
|
<Button
|
|
151
166
|
variant="ghost"
|
|
152
167
|
size="sm"
|
|
153
|
-
className="h-8 w-8 p-0 text-destructive hover:text-destructive
|
|
168
|
+
className="h-8 w-8 p-0 text-destructive hover:text-destructive
|
|
154
169
|
hover:scale-110 active:scale-95 transition-transform duration-200"
|
|
155
170
|
onClick={(e) => {
|
|
156
171
|
e.stopPropagation();
|
|
157
172
|
deleteSession(session.id);
|
|
158
173
|
}}
|
|
159
|
-
title=
|
|
174
|
+
title={labels.delete}
|
|
160
175
|
>
|
|
161
176
|
<Trash2 className="h-4 w-4" />
|
|
162
177
|
</Button>
|
|
@@ -168,14 +183,14 @@ export const SessionList: React.FC<SessionListProps> = ({
|
|
|
168
183
|
{isLoadingMore && (
|
|
169
184
|
<div className="flex items-center justify-center py-6">
|
|
170
185
|
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground mr-2" />
|
|
171
|
-
<span className="text-sm text-muted-foreground">
|
|
186
|
+
<span className="text-sm text-muted-foreground">{labels.loadingMore}</span>
|
|
172
187
|
</div>
|
|
173
188
|
)}
|
|
174
|
-
|
|
189
|
+
|
|
175
190
|
{/* No more sessions indicator */}
|
|
176
191
|
{!hasMore && sessions.length > 0 && (
|
|
177
192
|
<div className="text-center py-4">
|
|
178
|
-
<span className="text-xs text-muted-foreground">
|
|
193
|
+
<span className="text-xs text-muted-foreground">{labels.noMore}</span>
|
|
179
194
|
</div>
|
|
180
195
|
)}
|
|
181
196
|
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowbase Extension I18n
|
|
3
|
+
*
|
|
4
|
+
* Self-contained translations - no app configuration needed.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { useKnowbaseT } from '@djangocfg/ext-knowbase/i18n';
|
|
9
|
+
*
|
|
10
|
+
* function MyComponent() {
|
|
11
|
+
* const t = useKnowbaseT();
|
|
12
|
+
* return <span>{t('chat.title')}</span>;
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Self-contained hook (recommended)
|
|
18
|
+
export { useKnowbaseT } from './useKnowbaseT';
|
|
19
|
+
|
|
20
|
+
// Types
|
|
21
|
+
export type { KnowbaseTranslations, KnowbaseLocalKeys } from './types';
|
|
22
|
+
|
|
23
|
+
// Locales (for direct access if needed)
|
|
24
|
+
export { en } from './locales/en';
|
|
25
|
+
export { ru } from './locales/ru';
|
|
26
|
+
export { ko } from './locales/ko';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { KnowbaseTranslations } from '../types';
|
|
2
|
+
|
|
3
|
+
export const en: KnowbaseTranslations = {
|
|
4
|
+
chat: {
|
|
5
|
+
title: 'Knowledge Assistant',
|
|
6
|
+
titleShort: 'Support',
|
|
7
|
+
placeholder: 'Ask me anything...',
|
|
8
|
+
enterToSend: 'Press Enter to send, Shift+Enter for new line',
|
|
9
|
+
thinking: 'Thinking...',
|
|
10
|
+
source: 'Source {index}',
|
|
11
|
+
startConversation: 'Start a Conversation',
|
|
12
|
+
startConversationDescription: 'Ask me anything about the documentation, features, or get help with your project.',
|
|
13
|
+
openChat: 'Open Support Chat',
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
sessions: {
|
|
17
|
+
title: 'Chat Sessions',
|
|
18
|
+
description: 'View and manage your chat history',
|
|
19
|
+
noSessions: 'No Sessions Yet',
|
|
20
|
+
noSessionsDescription: 'Start a new conversation to create your first session.',
|
|
21
|
+
untitled: 'Untitled Session',
|
|
22
|
+
active: 'Active',
|
|
23
|
+
archive: 'Archive',
|
|
24
|
+
delete: 'Delete',
|
|
25
|
+
loadingMore: 'Loading more...',
|
|
26
|
+
noMore: 'No more sessions',
|
|
27
|
+
newChat: 'New Chat',
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
actions: {
|
|
31
|
+
sessions: 'Sessions',
|
|
32
|
+
collapse: 'Collapse',
|
|
33
|
+
expand: 'Expand',
|
|
34
|
+
close: 'Close',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { KnowbaseTranslations } from '../types';
|
|
2
|
+
|
|
3
|
+
export const ko: KnowbaseTranslations = {
|
|
4
|
+
chat: {
|
|
5
|
+
title: '지식 도우미',
|
|
6
|
+
titleShort: '지원',
|
|
7
|
+
placeholder: '무엇이든 물어보세요...',
|
|
8
|
+
enterToSend: 'Enter로 전송, Shift+Enter로 줄바꿈',
|
|
9
|
+
thinking: '생각 중...',
|
|
10
|
+
source: '출처 {index}',
|
|
11
|
+
startConversation: '대화 시작하기',
|
|
12
|
+
startConversationDescription: '문서, 기능에 대해 질문하거나 프로젝트 도움을 받으세요.',
|
|
13
|
+
openChat: '지원 채팅 열기',
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
sessions: {
|
|
17
|
+
title: '채팅 세션',
|
|
18
|
+
description: '채팅 기록 보기 및 관리',
|
|
19
|
+
noSessions: '세션 없음',
|
|
20
|
+
noSessionsDescription: '새 대화를 시작하여 첫 번째 세션을 만드세요.',
|
|
21
|
+
untitled: '제목 없음',
|
|
22
|
+
active: '활성',
|
|
23
|
+
archive: '보관',
|
|
24
|
+
delete: '삭제',
|
|
25
|
+
loadingMore: '불러오는 중...',
|
|
26
|
+
noMore: '더 이상 세션 없음',
|
|
27
|
+
newChat: '새 채팅',
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
actions: {
|
|
31
|
+
sessions: '세션',
|
|
32
|
+
collapse: '축소',
|
|
33
|
+
expand: '확장',
|
|
34
|
+
close: '닫기',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { KnowbaseTranslations } from '../types';
|
|
2
|
+
|
|
3
|
+
export const ru: KnowbaseTranslations = {
|
|
4
|
+
chat: {
|
|
5
|
+
title: 'База знаний',
|
|
6
|
+
titleShort: 'Поддержка',
|
|
7
|
+
placeholder: 'Задайте вопрос...',
|
|
8
|
+
enterToSend: 'Enter для отправки, Shift+Enter для новой строки',
|
|
9
|
+
thinking: 'Думаю...',
|
|
10
|
+
source: 'Источник {index}',
|
|
11
|
+
startConversation: 'Начать разговор',
|
|
12
|
+
startConversationDescription: 'Задайте любой вопрос о документации, функциях или получите помощь с проектом.',
|
|
13
|
+
openChat: 'Открыть чат поддержки',
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
sessions: {
|
|
17
|
+
title: 'Сессии чата',
|
|
18
|
+
description: 'Просмотр и управление историей чата',
|
|
19
|
+
noSessions: 'Нет сессий',
|
|
20
|
+
noSessionsDescription: 'Начните новый разговор, чтобы создать первую сессию.',
|
|
21
|
+
untitled: 'Без названия',
|
|
22
|
+
active: 'Активная',
|
|
23
|
+
archive: 'Архивировать',
|
|
24
|
+
delete: 'Удалить',
|
|
25
|
+
loadingMore: 'Загрузка...',
|
|
26
|
+
noMore: 'Больше нет сессий',
|
|
27
|
+
newChat: 'Новый чат',
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
actions: {
|
|
31
|
+
sessions: 'Сессии',
|
|
32
|
+
collapse: 'Свернуть',
|
|
33
|
+
expand: 'Развернуть',
|
|
34
|
+
close: 'Закрыть',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowbase Extension I18n Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper type to get dot-notation paths from nested object
|
|
7
|
+
*/
|
|
8
|
+
type PathKeys<T, Prefix extends string = ''> = T extends object
|
|
9
|
+
? {
|
|
10
|
+
[K in keyof T]: K extends string
|
|
11
|
+
? T[K] extends object
|
|
12
|
+
? PathKeys<T[K], `${Prefix}${K}.`>
|
|
13
|
+
: `${Prefix}${K}`
|
|
14
|
+
: never;
|
|
15
|
+
}[keyof T]
|
|
16
|
+
: never;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Keys for knowbase translations
|
|
20
|
+
*/
|
|
21
|
+
export type KnowbaseLocalKeys = PathKeys<KnowbaseTranslations>;
|
|
22
|
+
|
|
23
|
+
export interface KnowbaseTranslations {
|
|
24
|
+
/** Chat widget */
|
|
25
|
+
chat: {
|
|
26
|
+
title: string;
|
|
27
|
+
titleShort: string;
|
|
28
|
+
placeholder: string;
|
|
29
|
+
enterToSend: string;
|
|
30
|
+
thinking: string;
|
|
31
|
+
source: string;
|
|
32
|
+
startConversation: string;
|
|
33
|
+
startConversationDescription: string;
|
|
34
|
+
openChat: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Sessions */
|
|
38
|
+
sessions: {
|
|
39
|
+
title: string;
|
|
40
|
+
description: string;
|
|
41
|
+
noSessions: string;
|
|
42
|
+
noSessionsDescription: string;
|
|
43
|
+
untitled: string;
|
|
44
|
+
active: string;
|
|
45
|
+
archive: string;
|
|
46
|
+
delete: string;
|
|
47
|
+
loadingMore: string;
|
|
48
|
+
noMore: string;
|
|
49
|
+
newChat: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Actions */
|
|
53
|
+
actions: {
|
|
54
|
+
sessions: string;
|
|
55
|
+
collapse: string;
|
|
56
|
+
expand: string;
|
|
57
|
+
close: string;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Self-contained translation hook for ext-knowbase
|
|
5
|
+
*
|
|
6
|
+
* Uses built-in translations, no app configuration needed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useLocale } from 'next-intl';
|
|
10
|
+
import { useMemo, useCallback } from 'react';
|
|
11
|
+
|
|
12
|
+
import type { KnowbaseTranslations, KnowbaseLocalKeys } from './types';
|
|
13
|
+
import { en } from './locales/en';
|
|
14
|
+
import { ru } from './locales/ru';
|
|
15
|
+
import { ko } from './locales/ko';
|
|
16
|
+
|
|
17
|
+
const translations: Record<string, KnowbaseTranslations> = { en, ru, ko };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get nested value from object by dot-notation path
|
|
21
|
+
*/
|
|
22
|
+
function getNestedValue(obj: Record<string, unknown>, path: string): string {
|
|
23
|
+
const keys = path.split('.');
|
|
24
|
+
let result: unknown = obj;
|
|
25
|
+
|
|
26
|
+
for (const key of keys) {
|
|
27
|
+
if (result && typeof result === 'object' && key in result) {
|
|
28
|
+
result = (result as Record<string, unknown>)[key];
|
|
29
|
+
} else {
|
|
30
|
+
return path; // Return key if not found
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return typeof result === 'string' ? result : path;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Self-contained translation hook for knowbase extension
|
|
39
|
+
*
|
|
40
|
+
* Uses built-in translations based on current locale from next-intl.
|
|
41
|
+
* No need to add translations to app's i18n config.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* function ChatWidget() {
|
|
46
|
+
* const t = useKnowbaseT();
|
|
47
|
+
* return <h1>{t('chat.title')}</h1>;
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function useKnowbaseT(): (key: KnowbaseLocalKeys) => string {
|
|
52
|
+
const locale = useLocale();
|
|
53
|
+
|
|
54
|
+
const t = useMemo(() => translations[locale] || translations.en, [locale]);
|
|
55
|
+
|
|
56
|
+
return useCallback(
|
|
57
|
+
(key: KnowbaseLocalKeys): string => getNestedValue(t as unknown as Record<string, unknown>, key),
|
|
58
|
+
[t]
|
|
59
|
+
);
|
|
60
|
+
}
|