@tuturuuu/ui 0.3.1 → 0.4.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/CHANGELOG.md +15 -0
- package/package.json +7 -7
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +396 -2
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +36 -8
- package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +14 -0
- package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +5 -0
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +21 -7
- package/src/components/ui/chat/chat-agent-details-utils.test.ts +73 -0
- package/src/components/ui/chat/chat-agent-details-utils.tsx +100 -26
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +517 -0
- package/src/components/ui/chat/chat-sidebar-groups.test.ts +13 -3
- package/src/components/ui/chat/chat-sidebar-panel.test.tsx +56 -2
- package/src/components/ui/chat/chat-sidebar-panel.tsx +36 -19
- package/src/components/ui/chat/chat-sidebar.tsx +7 -0
- package/src/components/ui/chat/chat-utils.test.ts +14 -1
- package/src/components/ui/chat/chat-workspace.tsx +115 -44
- package/src/components/ui/chat/create-conversation-dialog-utils.tsx +56 -0
- package/src/components/ui/chat/create-conversation-dialog.test.tsx +105 -0
- package/src/components/ui/chat/create-conversation-dialog.tsx +176 -170
- package/src/components/ui/chat/create-integration-panel.tsx +110 -0
- package/src/components/ui/chat/hooks-integrations.ts +28 -0
- package/src/components/ui/chat/hooks-messages.test.tsx +45 -1
- package/src/components/ui/chat/hooks-messages.ts +1 -1
- package/src/components/ui/chat/hooks-realtime.ts +13 -16
- package/src/components/ui/chat/hooks.ts +1 -0
- package/src/components/ui/chat/selection.ts +74 -0
- package/src/components/ui/chat/utils.ts +7 -1
- package/src/hooks/use-semantic-task-search.ts +10 -33
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChatConversation } from '@tuturuuu/internal-api';
|
|
2
|
-
import { usePathname, useSearchParams } from 'next/navigation';
|
|
2
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import { ChatSidebar } from './chat-sidebar';
|
|
5
5
|
import { CreateConversationDialog } from './create-conversation-dialog';
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useChatMessageSearch,
|
|
9
9
|
useInfiniteChatConversations,
|
|
10
10
|
} from './hooks';
|
|
11
|
+
import { type ChatDetailsTarget, replaceChatSelection } from './selection';
|
|
11
12
|
import {
|
|
12
13
|
CHAT_CONVERSATION_TYPE_FILTERS,
|
|
13
14
|
type ChatConversationArchiveFilter,
|
|
@@ -26,6 +27,7 @@ interface ChatSidebarPanelProps {
|
|
|
26
27
|
createOpen?: boolean;
|
|
27
28
|
currentUserId: string;
|
|
28
29
|
defaultConversationScope?: ChatConversationScope;
|
|
30
|
+
enableRootIntegrations?: boolean;
|
|
29
31
|
isCollapsed: boolean;
|
|
30
32
|
onCreateOpenChange?: (open: boolean) => void;
|
|
31
33
|
onSearchChange?: (value: string) => void;
|
|
@@ -40,6 +42,7 @@ export function ChatSidebarPanel({
|
|
|
40
42
|
createOpen: controlledCreateOpen,
|
|
41
43
|
currentUserId,
|
|
42
44
|
defaultConversationScope = 'personal',
|
|
45
|
+
enableRootIntegrations,
|
|
43
46
|
isCollapsed,
|
|
44
47
|
onCreateOpenChange,
|
|
45
48
|
onSearchChange,
|
|
@@ -48,6 +51,7 @@ export function ChatSidebarPanel({
|
|
|
48
51
|
wsId,
|
|
49
52
|
}: ChatSidebarPanelProps) {
|
|
50
53
|
const pathname = usePathname();
|
|
54
|
+
const router = useRouter();
|
|
51
55
|
const searchParams = useSearchParams();
|
|
52
56
|
const [internalSearchValue, setInternalSearchValue] = useState('');
|
|
53
57
|
const [internalCreateOpen, setInternalCreateOpen] = useState(false);
|
|
@@ -101,6 +105,11 @@ export function ChatSidebarPanel({
|
|
|
101
105
|
wsId,
|
|
102
106
|
conversationScope
|
|
103
107
|
);
|
|
108
|
+
const requestedConversationPending = Boolean(
|
|
109
|
+
requestedConversationId &&
|
|
110
|
+
!scopedConversationIdList.includes(requestedConversationId) &&
|
|
111
|
+
conversationsQuery.isFetching
|
|
112
|
+
);
|
|
104
113
|
const selectedConversationId = resolveChatConversationSelection({
|
|
105
114
|
conversationIds: scopedConversationIdList,
|
|
106
115
|
requestedConversationId,
|
|
@@ -116,37 +125,41 @@ export function ChatSidebarPanel({
|
|
|
116
125
|
useEffect(() => {
|
|
117
126
|
if (!selectedConversationId) return;
|
|
118
127
|
if (!storedSelectionLoaded && !requestedConversationId) return;
|
|
128
|
+
if (requestedConversationPending) return;
|
|
119
129
|
|
|
120
130
|
localStorage.setItem(selectionStorageKey, selectedConversationId);
|
|
121
131
|
if (requestedConversationId === selectedConversationId) return;
|
|
122
132
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
133
|
+
replaceChatSelection({
|
|
134
|
+
conversationId: selectedConversationId,
|
|
135
|
+
pathname,
|
|
136
|
+
router,
|
|
137
|
+
searchParams,
|
|
138
|
+
storageKey: selectionStorageKey,
|
|
139
|
+
});
|
|
131
140
|
}, [
|
|
132
141
|
pathname,
|
|
133
142
|
requestedConversationId,
|
|
143
|
+
router,
|
|
134
144
|
searchParams,
|
|
135
145
|
selectedConversationId,
|
|
136
146
|
selectionStorageKey,
|
|
137
147
|
storedSelectionLoaded,
|
|
148
|
+
requestedConversationPending,
|
|
138
149
|
]);
|
|
139
150
|
|
|
140
|
-
function selectConversation(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
function selectConversation(
|
|
152
|
+
conversationId: string,
|
|
153
|
+
details: ChatDetailsTarget = null
|
|
154
|
+
) {
|
|
155
|
+
replaceChatSelection({
|
|
156
|
+
conversationId,
|
|
157
|
+
details,
|
|
158
|
+
pathname,
|
|
159
|
+
router,
|
|
160
|
+
searchParams,
|
|
161
|
+
storageKey: selectionStorageKey,
|
|
162
|
+
});
|
|
150
163
|
closeOnMobile?.();
|
|
151
164
|
}
|
|
152
165
|
|
|
@@ -183,7 +196,11 @@ export function ChatSidebarPanel({
|
|
|
183
196
|
conversationScope={conversationScope}
|
|
184
197
|
defaultType={getChatConversationTypesForScope(conversationScope)[0]}
|
|
185
198
|
currentUserId={currentUserId}
|
|
199
|
+
enableRootIntegrations={enableRootIntegrations}
|
|
186
200
|
onCreated={handleCreated}
|
|
201
|
+
onIntegrationCreated={(conversationId) =>
|
|
202
|
+
selectConversation(conversationId, 'agent')
|
|
203
|
+
}
|
|
187
204
|
onOpenChange={setCreateOpen}
|
|
188
205
|
open={createOpen}
|
|
189
206
|
wsId={wsId}
|
|
@@ -233,6 +233,13 @@ export function getChatConversationSections({
|
|
|
233
233
|
label: labels.group,
|
|
234
234
|
sectionType: 'group' as const,
|
|
235
235
|
},
|
|
236
|
+
{
|
|
237
|
+
conversations: conversations.filter(
|
|
238
|
+
(conversation) => conversation.type === 'channel'
|
|
239
|
+
),
|
|
240
|
+
label: labels.channel,
|
|
241
|
+
sectionType: 'channel' as const,
|
|
242
|
+
},
|
|
236
243
|
{
|
|
237
244
|
conversations: conversations.filter(
|
|
238
245
|
(conversation) => conversation.type === 'ai'
|
|
@@ -181,6 +181,11 @@ describe('chat utils', () => {
|
|
|
181
181
|
const direct = conversation({ id: 'direct-1', type: 'direct' });
|
|
182
182
|
const group = conversation({ id: 'group-1', type: 'group' });
|
|
183
183
|
const channel = conversation({ id: 'channel-1', type: 'channel' });
|
|
184
|
+
const personalChannel = conversation({
|
|
185
|
+
id: 'personal-channel-1',
|
|
186
|
+
metadata: { scope: 'personal' },
|
|
187
|
+
type: 'channel',
|
|
188
|
+
});
|
|
184
189
|
const ai = conversation({ id: 'ai-1', type: 'ai' });
|
|
185
190
|
|
|
186
191
|
expect(normalizeChatConversationScope('workspaces')).toBe('workspaces');
|
|
@@ -188,6 +193,7 @@ describe('chat utils', () => {
|
|
|
188
193
|
expect(getChatConversationTypesForScope('personal')).toEqual([
|
|
189
194
|
'direct',
|
|
190
195
|
'group',
|
|
196
|
+
'channel',
|
|
191
197
|
'ai',
|
|
192
198
|
]);
|
|
193
199
|
expect(getChatConversationTypesForScope('workspaces')).toEqual([
|
|
@@ -206,6 +212,7 @@ describe('chat utils', () => {
|
|
|
206
212
|
[
|
|
207
213
|
direct,
|
|
208
214
|
group,
|
|
215
|
+
personalChannel,
|
|
209
216
|
channel,
|
|
210
217
|
conversation({
|
|
211
218
|
id: 'ai-chat-1',
|
|
@@ -220,7 +227,13 @@ describe('chat utils', () => {
|
|
|
220
227
|
],
|
|
221
228
|
'personal'
|
|
222
229
|
).map((item) => item.id)
|
|
223
|
-
).toEqual([
|
|
230
|
+
).toEqual([
|
|
231
|
+
'direct-1',
|
|
232
|
+
'group-1',
|
|
233
|
+
'personal-channel-1',
|
|
234
|
+
'ai-chat-1',
|
|
235
|
+
'personal-ai-1',
|
|
236
|
+
]);
|
|
224
237
|
expect(
|
|
225
238
|
filterChatConversationsByScope(
|
|
226
239
|
[
|
|
@@ -5,9 +5,10 @@ import type {
|
|
|
5
5
|
ChatAttachment,
|
|
6
6
|
ChatAttachmentDraft,
|
|
7
7
|
ChatConversation,
|
|
8
|
+
ChatMessage,
|
|
8
9
|
} from '@tuturuuu/internal-api';
|
|
9
10
|
import { cn } from '@tuturuuu/utils/format';
|
|
10
|
-
import { usePathname, useSearchParams } from 'next/navigation';
|
|
11
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
11
12
|
import { useTranslations } from 'next-intl';
|
|
12
13
|
import { useEffect, useMemo, useState } from 'react';
|
|
13
14
|
import { Badge } from '../badge';
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
} from './hooks';
|
|
40
41
|
import { MessageComposer } from './message-composer';
|
|
41
42
|
import { MessageList } from './message-list';
|
|
43
|
+
import { type ChatDetailsTarget, replaceChatSelection } from './selection';
|
|
42
44
|
import {
|
|
43
45
|
CHAT_CONVERSATION_TYPE_FILTERS,
|
|
44
46
|
type ChatConversationArchiveFilter,
|
|
@@ -56,6 +58,7 @@ interface ChatWorkspaceProps {
|
|
|
56
58
|
className?: string;
|
|
57
59
|
defaultConversationScope?: ChatConversationScope;
|
|
58
60
|
currentUserId: string;
|
|
61
|
+
enableRootIntegrations?: boolean;
|
|
59
62
|
showSidebar?: boolean;
|
|
60
63
|
variant?: 'standalone' | 'web';
|
|
61
64
|
wsId: string;
|
|
@@ -65,12 +68,14 @@ export function ChatWorkspace({
|
|
|
65
68
|
className,
|
|
66
69
|
defaultConversationScope,
|
|
67
70
|
currentUserId,
|
|
71
|
+
enableRootIntegrations,
|
|
68
72
|
showSidebar = true,
|
|
69
73
|
variant = 'web',
|
|
70
74
|
wsId,
|
|
71
75
|
}: ChatWorkspaceProps) {
|
|
72
76
|
const t = useTranslations('chat');
|
|
73
77
|
const pathname = usePathname();
|
|
78
|
+
const router = useRouter();
|
|
74
79
|
const searchParams = useSearchParams();
|
|
75
80
|
const [searchValue, setSearchValue] = useState('');
|
|
76
81
|
const [createOpen, setCreateOpen] = useState(false);
|
|
@@ -118,20 +123,34 @@ export function ChatWorkspace({
|
|
|
118
123
|
const selectionStorageKey = conversationScope
|
|
119
124
|
? getChatSelectionStorageKey(wsId, conversationScope)
|
|
120
125
|
: null;
|
|
126
|
+
const requestedConversationPending = Boolean(
|
|
127
|
+
requestedConversationId &&
|
|
128
|
+
!conversationIds.has(requestedConversationId) &&
|
|
129
|
+
conversationsQuery.isFetching
|
|
130
|
+
);
|
|
121
131
|
const selectedConversationId = resolveChatConversationSelection({
|
|
122
132
|
conversationIds: conversationIdList,
|
|
123
133
|
requestedConversationId,
|
|
124
134
|
storedConversationId,
|
|
125
135
|
});
|
|
126
|
-
const selectedConversation = useMemo(
|
|
127
|
-
()
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
const selectedConversation = useMemo(() => {
|
|
137
|
+
if (requestedConversationPending) return null;
|
|
138
|
+
|
|
139
|
+
if (selectedConversationId && conversationIds.has(selectedConversationId)) {
|
|
140
|
+
return (
|
|
141
|
+
conversations.find((item) => item.id === selectedConversationId) ??
|
|
142
|
+
conversations[0] ??
|
|
143
|
+
null
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return conversations[0] ?? null;
|
|
148
|
+
}, [
|
|
149
|
+
conversationIds,
|
|
150
|
+
conversations,
|
|
151
|
+
requestedConversationPending,
|
|
152
|
+
selectedConversationId,
|
|
153
|
+
]);
|
|
135
154
|
const activeConversationId = selectedConversation?.id ?? null;
|
|
136
155
|
const activeNativeConversationId = isPostgresUuid(activeConversationId)
|
|
137
156
|
? activeConversationId
|
|
@@ -152,11 +171,17 @@ export function ChatWorkspace({
|
|
|
152
171
|
conversationId: selectedVirtualReadOnly ? null : activeConversationId,
|
|
153
172
|
wsId,
|
|
154
173
|
});
|
|
174
|
+
const fetchedMessages = selectedVirtualReadOnly
|
|
175
|
+
? []
|
|
176
|
+
: flattenChatMessagePages(messagesQuery.data);
|
|
155
177
|
const messages = selectedVirtualReadOnly
|
|
156
178
|
? selectedConversation?.latestMessage
|
|
157
179
|
? [selectedConversation.latestMessage]
|
|
158
180
|
: []
|
|
159
|
-
:
|
|
181
|
+
: mergeConversationLatestMessage(
|
|
182
|
+
fetchedMessages,
|
|
183
|
+
selectedConversation?.latestMessage
|
|
184
|
+
);
|
|
160
185
|
const sendMessage = useSendChatMessage({
|
|
161
186
|
conversationId: activeConversationId,
|
|
162
187
|
currentUserId,
|
|
@@ -201,7 +226,12 @@ export function ChatWorkspace({
|
|
|
201
226
|
const latestPersistedMessageId = isPostgresUuid(latestMessageId)
|
|
202
227
|
? latestMessageId
|
|
203
228
|
: null;
|
|
204
|
-
const
|
|
229
|
+
const requestedDetails = searchParams.get('details');
|
|
230
|
+
const agentDetailsOpen =
|
|
231
|
+
requestedDetails === 'agent' && selectedAgentReadOnly;
|
|
232
|
+
const detailsOpen = Boolean(
|
|
233
|
+
(sharedContentOpen || agentDetailsOpen) && activeConversationId
|
|
234
|
+
);
|
|
205
235
|
|
|
206
236
|
useChatRealtime(wsId);
|
|
207
237
|
|
|
@@ -220,6 +250,7 @@ export function ChatWorkspace({
|
|
|
220
250
|
useEffect(() => {
|
|
221
251
|
if (!activeConversationId) return;
|
|
222
252
|
if (!storedSelectionLoaded && !requestedConversationId) return;
|
|
253
|
+
if (requestedConversationPending) return;
|
|
223
254
|
|
|
224
255
|
if (selectionStorageKey) {
|
|
225
256
|
localStorage.setItem(selectionStorageKey, activeConversationId);
|
|
@@ -227,21 +258,22 @@ export function ChatWorkspace({
|
|
|
227
258
|
|
|
228
259
|
if (requestedConversationId === activeConversationId) return;
|
|
229
260
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
);
|
|
261
|
+
replaceChatSelection({
|
|
262
|
+
conversationId: activeConversationId,
|
|
263
|
+
pathname,
|
|
264
|
+
router,
|
|
265
|
+
searchParams,
|
|
266
|
+
storageKey: selectionStorageKey,
|
|
267
|
+
});
|
|
238
268
|
}, [
|
|
239
269
|
activeConversationId,
|
|
240
270
|
pathname,
|
|
241
271
|
requestedConversationId,
|
|
272
|
+
router,
|
|
242
273
|
searchParams,
|
|
243
274
|
selectionStorageKey,
|
|
244
275
|
storedSelectionLoaded,
|
|
276
|
+
requestedConversationPending,
|
|
245
277
|
]);
|
|
246
278
|
|
|
247
279
|
useEffect(() => {
|
|
@@ -303,18 +335,19 @@ export function ChatWorkspace({
|
|
|
303
335
|
}
|
|
304
336
|
}
|
|
305
337
|
|
|
306
|
-
function selectConversation(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
338
|
+
function selectConversation(
|
|
339
|
+
conversationId: string,
|
|
340
|
+
details: ChatDetailsTarget = null
|
|
341
|
+
) {
|
|
342
|
+
replaceChatSelection({
|
|
343
|
+
conversationId,
|
|
344
|
+
details,
|
|
345
|
+
pathname,
|
|
346
|
+
router,
|
|
347
|
+
searchParams,
|
|
348
|
+
storageKey: selectionStorageKey,
|
|
349
|
+
});
|
|
350
|
+
setSharedContentOpen(details === 'agent');
|
|
318
351
|
}
|
|
319
352
|
|
|
320
353
|
function handleCreated(conversation: ChatConversation) {
|
|
@@ -351,14 +384,13 @@ export function ChatWorkspace({
|
|
|
351
384
|
try {
|
|
352
385
|
await deleteConversation.mutateAsync(conversationId);
|
|
353
386
|
if (clearSelection) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
);
|
|
387
|
+
replaceChatSelection({
|
|
388
|
+
conversationId: null,
|
|
389
|
+
pathname,
|
|
390
|
+
router,
|
|
391
|
+
searchParams,
|
|
392
|
+
storageKey: selectionStorageKey,
|
|
393
|
+
});
|
|
362
394
|
}
|
|
363
395
|
toast.success(t('conversation_archived'));
|
|
364
396
|
} catch {
|
|
@@ -473,11 +505,23 @@ export function ChatWorkspace({
|
|
|
473
505
|
isUpdatingConversation={updateConversation.isPending}
|
|
474
506
|
onDeleteConversation={handleDeleteConversation}
|
|
475
507
|
onGenerateConversationTitle={handleGenerateConversationTitle}
|
|
476
|
-
onToggleSharedContent={() =>
|
|
477
|
-
|
|
478
|
-
|
|
508
|
+
onToggleSharedContent={() => {
|
|
509
|
+
if (requestedDetails) {
|
|
510
|
+
replaceChatSelection({
|
|
511
|
+
conversationId: activeConversationId,
|
|
512
|
+
pathname,
|
|
513
|
+
router,
|
|
514
|
+
searchParams,
|
|
515
|
+
storageKey: selectionStorageKey,
|
|
516
|
+
});
|
|
517
|
+
setSharedContentOpen(false);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
setSharedContentOpen((current) => !current);
|
|
522
|
+
}}
|
|
479
523
|
onUpdateConversation={handleUpdateConversation}
|
|
480
|
-
sharedContentOpen={
|
|
524
|
+
sharedContentOpen={detailsOpen}
|
|
481
525
|
title={selectedTitle}
|
|
482
526
|
/>
|
|
483
527
|
|
|
@@ -558,7 +602,11 @@ export function ChatWorkspace({
|
|
|
558
602
|
: undefined
|
|
559
603
|
}
|
|
560
604
|
currentUserId={currentUserId}
|
|
605
|
+
enableRootIntegrations={enableRootIntegrations}
|
|
561
606
|
onCreated={handleCreated}
|
|
607
|
+
onIntegrationCreated={(conversationId) =>
|
|
608
|
+
selectConversation(conversationId, 'agent')
|
|
609
|
+
}
|
|
562
610
|
onOpenChange={setCreateOpen}
|
|
563
611
|
open={createOpen}
|
|
564
612
|
wsId={wsId}
|
|
@@ -567,6 +615,29 @@ export function ChatWorkspace({
|
|
|
567
615
|
);
|
|
568
616
|
}
|
|
569
617
|
|
|
618
|
+
function mergeConversationLatestMessage(
|
|
619
|
+
messages: ChatMessage[],
|
|
620
|
+
latestMessage?: ChatMessage | null
|
|
621
|
+
) {
|
|
622
|
+
if (
|
|
623
|
+
!latestMessage ||
|
|
624
|
+
messages.some((message) => message.id === latestMessage.id)
|
|
625
|
+
) {
|
|
626
|
+
return messages;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return [...messages, latestMessage].sort(
|
|
630
|
+
(left, right) =>
|
|
631
|
+
readChatMessageTimestamp(left.createdAt) -
|
|
632
|
+
readChatMessageTimestamp(right.createdAt)
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function readChatMessageTimestamp(value: string) {
|
|
637
|
+
const timestamp = Date.parse(value);
|
|
638
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
639
|
+
}
|
|
640
|
+
|
|
570
641
|
const POSTGRES_UUID_PATTERN =
|
|
571
642
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
572
643
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ChatConversationType,
|
|
3
|
+
InternalApiError,
|
|
4
|
+
} from '@tuturuuu/internal-api';
|
|
5
|
+
import type { useTranslations } from 'next-intl';
|
|
6
|
+
import type { ChatConversationScope } from './utils';
|
|
7
|
+
|
|
8
|
+
export function getConversationMetadata(
|
|
9
|
+
conversationScope: ChatConversationScope | undefined,
|
|
10
|
+
type: ChatConversationType
|
|
11
|
+
) {
|
|
12
|
+
if (conversationScope !== 'personal') return undefined;
|
|
13
|
+
if (type === 'ai') return { source: 'personal-ai-chat' };
|
|
14
|
+
if (type === 'channel') return { scope: 'personal' };
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function StepTitle({
|
|
19
|
+
description,
|
|
20
|
+
title,
|
|
21
|
+
}: {
|
|
22
|
+
description: string;
|
|
23
|
+
title: string;
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<div>
|
|
27
|
+
<h3 className="font-medium text-sm">{title}</h3>
|
|
28
|
+
<p className="mt-1 text-muted-foreground text-xs">{description}</p>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getCreateConversationErrorDescription(
|
|
34
|
+
error: unknown,
|
|
35
|
+
t: ReturnType<typeof useTranslations>
|
|
36
|
+
) {
|
|
37
|
+
if (!(error instanceof InternalApiError)) return undefined;
|
|
38
|
+
|
|
39
|
+
if (error.message.includes('chat_target_not_invitable')) {
|
|
40
|
+
return t('conversation_create_target_not_invitable');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (error.message.includes('chat_direct_requires_one_target')) {
|
|
44
|
+
return t('conversation_create_direct_requires_one_target');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (error.message.includes('chat_group_requires_members')) {
|
|
48
|
+
return t('conversation_create_group_requires_members');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (error.message.includes('chat_permission_required')) {
|
|
52
|
+
return t('conversation_create_permission_required');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return error.message;
|
|
56
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import type { ChatConversation } from '@tuturuuu/internal-api';
|
|
3
|
+
import { ROOT_WORKSPACE_ID } from '@tuturuuu/utils/constants';
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { CreateConversationDialog } from './create-conversation-dialog';
|
|
6
|
+
import { CreateIntegrationPanel } from './create-integration-panel';
|
|
5
7
|
|
|
6
8
|
const mocks = vi.hoisted(() => ({
|
|
7
9
|
createConversation: vi.fn(),
|
|
8
10
|
createFriendRequest: vi.fn(),
|
|
11
|
+
createIntegration: vi.fn(),
|
|
9
12
|
}));
|
|
10
13
|
|
|
11
14
|
vi.mock('next-intl', () => ({
|
|
@@ -25,6 +28,18 @@ vi.mock('./hooks', () => ({
|
|
|
25
28
|
isPending: false,
|
|
26
29
|
mutateAsync: mocks.createFriendRequest,
|
|
27
30
|
}),
|
|
31
|
+
useCreateChatIntegration: () => ({
|
|
32
|
+
isPending: false,
|
|
33
|
+
mutate: mocks.createIntegration,
|
|
34
|
+
variables: null,
|
|
35
|
+
}),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock('../sonner', () => ({
|
|
39
|
+
toast: {
|
|
40
|
+
error: vi.fn(),
|
|
41
|
+
success: vi.fn(),
|
|
42
|
+
},
|
|
28
43
|
}));
|
|
29
44
|
|
|
30
45
|
const createdConversation: ChatConversation = {
|
|
@@ -53,6 +68,13 @@ describe('CreateConversationDialog', () => {
|
|
|
53
68
|
mocks.createConversation.mockResolvedValue({
|
|
54
69
|
conversation: createdConversation,
|
|
55
70
|
});
|
|
71
|
+
mocks.createIntegration.mockImplementation((_payload, options) => {
|
|
72
|
+
options?.onSuccess?.({
|
|
73
|
+
agent: { id: 'chat-integrations' },
|
|
74
|
+
channel: { id: 'chat-zalo-personal' },
|
|
75
|
+
conversationId: 'ai-agent-chat-integrations-chat-zalo-personal',
|
|
76
|
+
});
|
|
77
|
+
});
|
|
56
78
|
});
|
|
57
79
|
|
|
58
80
|
it('creates personal AI chats with personal AI metadata', async () => {
|
|
@@ -86,4 +108,87 @@ describe('CreateConversationDialog', () => {
|
|
|
86
108
|
});
|
|
87
109
|
expect(onCreated).toHaveBeenCalledWith(createdConversation);
|
|
88
110
|
});
|
|
111
|
+
|
|
112
|
+
it('creates personal channels with personal scope metadata', async () => {
|
|
113
|
+
render(
|
|
114
|
+
<CreateConversationDialog
|
|
115
|
+
conversationScope="personal"
|
|
116
|
+
currentUserId="user-1"
|
|
117
|
+
onCreated={vi.fn()}
|
|
118
|
+
onOpenChange={vi.fn()}
|
|
119
|
+
open
|
|
120
|
+
wsId="personal-workspace-1"
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
fireEvent.click(screen.getByText('type_channel'));
|
|
125
|
+
fireEvent.click(screen.getByText('next'));
|
|
126
|
+
fireEvent.change(screen.getByPlaceholderText('channel_name_placeholder'), {
|
|
127
|
+
target: { value: 'Ideas' },
|
|
128
|
+
});
|
|
129
|
+
fireEvent.click(screen.getByText('create'));
|
|
130
|
+
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(mocks.createConversation).toHaveBeenCalledWith(
|
|
133
|
+
expect.objectContaining({
|
|
134
|
+
aiEnabled: false,
|
|
135
|
+
metadata: {
|
|
136
|
+
scope: 'personal',
|
|
137
|
+
},
|
|
138
|
+
title: 'Ideas',
|
|
139
|
+
type: 'channel',
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('hides the integrations tab outside the internal root workspace', () => {
|
|
146
|
+
render(
|
|
147
|
+
<CreateConversationDialog
|
|
148
|
+
currentUserId="user-1"
|
|
149
|
+
enableRootIntegrations
|
|
150
|
+
onCreated={vi.fn()}
|
|
151
|
+
onOpenChange={vi.fn()}
|
|
152
|
+
open
|
|
153
|
+
wsId="workspace-1"
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(screen.queryByText('tab_integrations')).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('shows the integrations tab in the internal root workspace', () => {
|
|
161
|
+
render(
|
|
162
|
+
<CreateConversationDialog
|
|
163
|
+
currentUserId="user-1"
|
|
164
|
+
enableRootIntegrations
|
|
165
|
+
onCreated={vi.fn()}
|
|
166
|
+
onOpenChange={vi.fn()}
|
|
167
|
+
open
|
|
168
|
+
wsId={ROOT_WORKSPACE_ID}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
expect(screen.getByRole('tab', { name: 'tab_integrations' })).toBeTruthy();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('selects the returned virtual agent conversation after integration setup', () => {
|
|
176
|
+
const onCreated = vi.fn();
|
|
177
|
+
|
|
178
|
+
render(
|
|
179
|
+
<CreateIntegrationPanel onCreated={(result) => onCreated(result)} />
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
fireEvent.click(screen.getByText('integration_zalo_personal'));
|
|
183
|
+
|
|
184
|
+
expect(mocks.createIntegration).toHaveBeenCalledWith(
|
|
185
|
+
{ kind: 'zalo-personal' },
|
|
186
|
+
expect.any(Object)
|
|
187
|
+
);
|
|
188
|
+
expect(onCreated).toHaveBeenCalledWith(
|
|
189
|
+
expect.objectContaining({
|
|
190
|
+
conversationId: 'ai-agent-chat-integrations-chat-zalo-personal',
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
});
|
|
89
194
|
});
|