@tuturuuu/ui 0.3.2 → 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 +8 -0
- package/package.json +6 -6
- 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 +84 -43
- 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.ts +1 -0
- package/src/components/ui/chat/selection.ts +74 -0
- package/src/components/ui/chat/utils.ts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/tutur3u/platform/compare/ui-v0.3.2...ui-v0.4.0) (2026-06-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **chat:** add personal channels and root integrations ([fb5e753](https://github.com/tutur3u/platform/commit/fb5e7534588c7015449313fc4a752b70732f227e))
|
|
9
|
+
* **chat:** merge personal channels and root integrations ([22d50ce](https://github.com/tutur3u/platform/commit/22d50ce0d75e36e0beaa973ef59cbd296e22dc35))
|
|
10
|
+
|
|
3
11
|
## [0.3.2](https://github.com/tutur3u/platform/compare/ui-v0.3.1...ui-v0.3.2) (2026-06-10)
|
|
4
12
|
|
|
5
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -82,14 +82,14 @@
|
|
|
82
82
|
"@tiptap/pm": "3.26.0",
|
|
83
83
|
"@tiptap/react": "3.26.0",
|
|
84
84
|
"@tiptap/starter-kit": "3.26.0",
|
|
85
|
-
"@tuturuuu/ai": "0.
|
|
85
|
+
"@tuturuuu/ai": "0.2.0",
|
|
86
86
|
"@tuturuuu/apis": "0.1.0",
|
|
87
87
|
"@tuturuuu/hooks": "0.0.1",
|
|
88
88
|
"@tuturuuu/icons": "0.0.5",
|
|
89
|
-
"@tuturuuu/internal-api": "0.
|
|
90
|
-
"@tuturuuu/supabase": "0.3.
|
|
89
|
+
"@tuturuuu/internal-api": "0.4.0",
|
|
90
|
+
"@tuturuuu/supabase": "0.3.3",
|
|
91
91
|
"@tuturuuu/trigger": "0.2.0",
|
|
92
|
-
"@tuturuuu/utils": "0.
|
|
92
|
+
"@tuturuuu/utils": "0.5.0",
|
|
93
93
|
"@types/debug": "^4.1.13",
|
|
94
94
|
"browser-image-compression": "^2.0.2",
|
|
95
95
|
"class-variance-authority": "^0.7.1",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"@tanstack/react-table": "^8.21.3",
|
|
149
149
|
"@testing-library/jest-dom": "^6.9.1",
|
|
150
150
|
"@testing-library/react": "^16.3.2",
|
|
151
|
-
"@tuturuuu/types": "0.
|
|
151
|
+
"@tuturuuu/types": "0.6.0",
|
|
152
152
|
"@tuturuuu/typescript-config": "0.1.1",
|
|
153
153
|
"@types/html2canvas": "^1.0.0",
|
|
154
154
|
"@types/lodash": "^4.17.24",
|
|
@@ -4,7 +4,10 @@ import { getChatConversationSections } from './chat-sidebar';
|
|
|
4
4
|
|
|
5
5
|
function conversation(
|
|
6
6
|
type: ChatConversation['type'],
|
|
7
|
-
id = `${type}-conversation
|
|
7
|
+
id = `${type}-conversation`,
|
|
8
|
+
metadata: ChatConversation['metadata'] = type === 'ai'
|
|
9
|
+
? { source: 'personal-ai-chat' }
|
|
10
|
+
: {}
|
|
8
11
|
): ChatConversation {
|
|
9
12
|
return {
|
|
10
13
|
aiEnabled: type === 'ai',
|
|
@@ -16,7 +19,7 @@ function conversation(
|
|
|
16
19
|
latestMessage: null,
|
|
17
20
|
memberCount: 1,
|
|
18
21
|
members: [],
|
|
19
|
-
metadata
|
|
22
|
+
metadata,
|
|
20
23
|
title: null,
|
|
21
24
|
type,
|
|
22
25
|
unreadCount: 0,
|
|
@@ -31,8 +34,10 @@ describe('chat sidebar conversation sections', () => {
|
|
|
31
34
|
conversations: [
|
|
32
35
|
conversation('direct'),
|
|
33
36
|
conversation('group'),
|
|
37
|
+
conversation('channel', 'channel-conversation', {
|
|
38
|
+
scope: 'personal',
|
|
39
|
+
}),
|
|
34
40
|
conversation('ai'),
|
|
35
|
-
conversation('channel'),
|
|
36
41
|
],
|
|
37
42
|
labels: {
|
|
38
43
|
ai: 'AI agents',
|
|
@@ -54,6 +59,11 @@ describe('chat sidebar conversation sections', () => {
|
|
|
54
59
|
label: 'Groups',
|
|
55
60
|
sectionType: 'group',
|
|
56
61
|
},
|
|
62
|
+
{
|
|
63
|
+
conversations: [{ id: 'channel-conversation' }],
|
|
64
|
+
label: 'Channels',
|
|
65
|
+
sectionType: 'channel',
|
|
66
|
+
},
|
|
57
67
|
{
|
|
58
68
|
conversations: [{ id: 'ai-conversation' }],
|
|
59
69
|
label: 'AI agents',
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import type { ChatConversation } from '@tuturuuu/internal-api';
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { ChatSidebarPanel } from './chat-sidebar-panel';
|
|
5
5
|
|
|
6
6
|
const mocks = vi.hoisted(() => ({
|
|
7
7
|
chatSidebarProps: null as Record<string, unknown> | null,
|
|
8
|
+
createDialogProps: null as Record<string, unknown> | null,
|
|
8
9
|
fetchNextPage: vi.fn(),
|
|
10
|
+
routerReplace: vi.fn(),
|
|
9
11
|
useChatMessageSearch: vi.fn(),
|
|
10
12
|
useInfiniteChatConversations: vi.fn(),
|
|
11
13
|
}));
|
|
@@ -16,6 +18,9 @@ vi.mock('next-intl', () => ({
|
|
|
16
18
|
|
|
17
19
|
vi.mock('next/navigation', () => ({
|
|
18
20
|
usePathname: () => '/personal',
|
|
21
|
+
useRouter: () => ({
|
|
22
|
+
replace: mocks.routerReplace,
|
|
23
|
+
}),
|
|
19
24
|
useSearchParams: () => new URLSearchParams('scope=personal'),
|
|
20
25
|
}));
|
|
21
26
|
|
|
@@ -36,7 +41,10 @@ vi.mock('./chat-sidebar', () => ({
|
|
|
36
41
|
}));
|
|
37
42
|
|
|
38
43
|
vi.mock('./create-conversation-dialog', () => ({
|
|
39
|
-
CreateConversationDialog: () =>
|
|
44
|
+
CreateConversationDialog: (props: Record<string, unknown>) => {
|
|
45
|
+
mocks.createDialogProps = props;
|
|
46
|
+
return null;
|
|
47
|
+
},
|
|
40
48
|
}));
|
|
41
49
|
|
|
42
50
|
vi.mock('./hooks', () => ({
|
|
@@ -71,6 +79,8 @@ describe('ChatSidebarPanel', () => {
|
|
|
71
79
|
beforeEach(() => {
|
|
72
80
|
vi.clearAllMocks();
|
|
73
81
|
mocks.chatSidebarProps = null;
|
|
82
|
+
mocks.createDialogProps = null;
|
|
83
|
+
window.localStorage.clear();
|
|
74
84
|
mocks.useChatMessageSearch.mockReturnValue({ data: [] });
|
|
75
85
|
mocks.useInfiniteChatConversations.mockReturnValue({
|
|
76
86
|
data: {
|
|
@@ -107,4 +117,48 @@ describe('ChatSidebarPanel', () => {
|
|
|
107
117
|
|
|
108
118
|
expect(mocks.fetchNextPage).toHaveBeenCalledTimes(1);
|
|
109
119
|
});
|
|
120
|
+
|
|
121
|
+
it('updates Next router state when auto-selecting the first conversation', async () => {
|
|
122
|
+
render(
|
|
123
|
+
<ChatSidebarPanel
|
|
124
|
+
currentUserId="user-1"
|
|
125
|
+
isCollapsed={false}
|
|
126
|
+
wsId="personal"
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
await waitFor(() => {
|
|
131
|
+
expect(mocks.routerReplace).toHaveBeenCalledWith(
|
|
132
|
+
'/personal?scope=personal&conversationId=conversation-1',
|
|
133
|
+
{ scroll: false }
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('opens agent details when an integration returns a virtual conversation', async () => {
|
|
139
|
+
render(
|
|
140
|
+
<ChatSidebarPanel
|
|
141
|
+
currentUserId="user-1"
|
|
142
|
+
enableRootIntegrations
|
|
143
|
+
isCollapsed={false}
|
|
144
|
+
wsId="personal"
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(mocks.createDialogProps).toBeTruthy();
|
|
150
|
+
});
|
|
151
|
+
mocks.routerReplace.mockClear();
|
|
152
|
+
|
|
153
|
+
(
|
|
154
|
+
mocks.createDialogProps?.onIntegrationCreated as
|
|
155
|
+
| ((conversationId: string) => void)
|
|
156
|
+
| undefined
|
|
157
|
+
)?.('ai-agent-chat-integrations-chat-zalo-personal');
|
|
158
|
+
|
|
159
|
+
expect(mocks.routerReplace).toHaveBeenCalledWith(
|
|
160
|
+
'/personal?scope=personal&conversationId=ai-agent-chat-integrations-chat-zalo-personal&details=agent',
|
|
161
|
+
{ scroll: false }
|
|
162
|
+
);
|
|
163
|
+
});
|
|
110
164
|
});
|
|
@@ -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
|
[
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
ChatMessage,
|
|
9
9
|
} from '@tuturuuu/internal-api';
|
|
10
10
|
import { cn } from '@tuturuuu/utils/format';
|
|
11
|
-
import { usePathname, useSearchParams } from 'next/navigation';
|
|
11
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
12
12
|
import { useTranslations } from 'next-intl';
|
|
13
13
|
import { useEffect, useMemo, useState } from 'react';
|
|
14
14
|
import { Badge } from '../badge';
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
} from './hooks';
|
|
41
41
|
import { MessageComposer } from './message-composer';
|
|
42
42
|
import { MessageList } from './message-list';
|
|
43
|
+
import { type ChatDetailsTarget, replaceChatSelection } from './selection';
|
|
43
44
|
import {
|
|
44
45
|
CHAT_CONVERSATION_TYPE_FILTERS,
|
|
45
46
|
type ChatConversationArchiveFilter,
|
|
@@ -57,6 +58,7 @@ interface ChatWorkspaceProps {
|
|
|
57
58
|
className?: string;
|
|
58
59
|
defaultConversationScope?: ChatConversationScope;
|
|
59
60
|
currentUserId: string;
|
|
61
|
+
enableRootIntegrations?: boolean;
|
|
60
62
|
showSidebar?: boolean;
|
|
61
63
|
variant?: 'standalone' | 'web';
|
|
62
64
|
wsId: string;
|
|
@@ -66,12 +68,14 @@ export function ChatWorkspace({
|
|
|
66
68
|
className,
|
|
67
69
|
defaultConversationScope,
|
|
68
70
|
currentUserId,
|
|
71
|
+
enableRootIntegrations,
|
|
69
72
|
showSidebar = true,
|
|
70
73
|
variant = 'web',
|
|
71
74
|
wsId,
|
|
72
75
|
}: ChatWorkspaceProps) {
|
|
73
76
|
const t = useTranslations('chat');
|
|
74
77
|
const pathname = usePathname();
|
|
78
|
+
const router = useRouter();
|
|
75
79
|
const searchParams = useSearchParams();
|
|
76
80
|
const [searchValue, setSearchValue] = useState('');
|
|
77
81
|
const [createOpen, setCreateOpen] = useState(false);
|
|
@@ -119,20 +123,34 @@ export function ChatWorkspace({
|
|
|
119
123
|
const selectionStorageKey = conversationScope
|
|
120
124
|
? getChatSelectionStorageKey(wsId, conversationScope)
|
|
121
125
|
: null;
|
|
126
|
+
const requestedConversationPending = Boolean(
|
|
127
|
+
requestedConversationId &&
|
|
128
|
+
!conversationIds.has(requestedConversationId) &&
|
|
129
|
+
conversationsQuery.isFetching
|
|
130
|
+
);
|
|
122
131
|
const selectedConversationId = resolveChatConversationSelection({
|
|
123
132
|
conversationIds: conversationIdList,
|
|
124
133
|
requestedConversationId,
|
|
125
134
|
storedConversationId,
|
|
126
135
|
});
|
|
127
|
-
const selectedConversation = useMemo(
|
|
128
|
-
()
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
]);
|
|
136
154
|
const activeConversationId = selectedConversation?.id ?? null;
|
|
137
155
|
const activeNativeConversationId = isPostgresUuid(activeConversationId)
|
|
138
156
|
? activeConversationId
|
|
@@ -208,7 +226,12 @@ export function ChatWorkspace({
|
|
|
208
226
|
const latestPersistedMessageId = isPostgresUuid(latestMessageId)
|
|
209
227
|
? latestMessageId
|
|
210
228
|
: null;
|
|
211
|
-
const
|
|
229
|
+
const requestedDetails = searchParams.get('details');
|
|
230
|
+
const agentDetailsOpen =
|
|
231
|
+
requestedDetails === 'agent' && selectedAgentReadOnly;
|
|
232
|
+
const detailsOpen = Boolean(
|
|
233
|
+
(sharedContentOpen || agentDetailsOpen) && activeConversationId
|
|
234
|
+
);
|
|
212
235
|
|
|
213
236
|
useChatRealtime(wsId);
|
|
214
237
|
|
|
@@ -227,6 +250,7 @@ export function ChatWorkspace({
|
|
|
227
250
|
useEffect(() => {
|
|
228
251
|
if (!activeConversationId) return;
|
|
229
252
|
if (!storedSelectionLoaded && !requestedConversationId) return;
|
|
253
|
+
if (requestedConversationPending) return;
|
|
230
254
|
|
|
231
255
|
if (selectionStorageKey) {
|
|
232
256
|
localStorage.setItem(selectionStorageKey, activeConversationId);
|
|
@@ -234,21 +258,22 @@ export function ChatWorkspace({
|
|
|
234
258
|
|
|
235
259
|
if (requestedConversationId === activeConversationId) return;
|
|
236
260
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
);
|
|
261
|
+
replaceChatSelection({
|
|
262
|
+
conversationId: activeConversationId,
|
|
263
|
+
pathname,
|
|
264
|
+
router,
|
|
265
|
+
searchParams,
|
|
266
|
+
storageKey: selectionStorageKey,
|
|
267
|
+
});
|
|
245
268
|
}, [
|
|
246
269
|
activeConversationId,
|
|
247
270
|
pathname,
|
|
248
271
|
requestedConversationId,
|
|
272
|
+
router,
|
|
249
273
|
searchParams,
|
|
250
274
|
selectionStorageKey,
|
|
251
275
|
storedSelectionLoaded,
|
|
276
|
+
requestedConversationPending,
|
|
252
277
|
]);
|
|
253
278
|
|
|
254
279
|
useEffect(() => {
|
|
@@ -310,18 +335,19 @@ export function ChatWorkspace({
|
|
|
310
335
|
}
|
|
311
336
|
}
|
|
312
337
|
|
|
313
|
-
function selectConversation(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
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');
|
|
325
351
|
}
|
|
326
352
|
|
|
327
353
|
function handleCreated(conversation: ChatConversation) {
|
|
@@ -358,14 +384,13 @@ export function ChatWorkspace({
|
|
|
358
384
|
try {
|
|
359
385
|
await deleteConversation.mutateAsync(conversationId);
|
|
360
386
|
if (clearSelection) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
);
|
|
387
|
+
replaceChatSelection({
|
|
388
|
+
conversationId: null,
|
|
389
|
+
pathname,
|
|
390
|
+
router,
|
|
391
|
+
searchParams,
|
|
392
|
+
storageKey: selectionStorageKey,
|
|
393
|
+
});
|
|
369
394
|
}
|
|
370
395
|
toast.success(t('conversation_archived'));
|
|
371
396
|
} catch {
|
|
@@ -480,11 +505,23 @@ export function ChatWorkspace({
|
|
|
480
505
|
isUpdatingConversation={updateConversation.isPending}
|
|
481
506
|
onDeleteConversation={handleDeleteConversation}
|
|
482
507
|
onGenerateConversationTitle={handleGenerateConversationTitle}
|
|
483
|
-
onToggleSharedContent={() =>
|
|
484
|
-
|
|
485
|
-
|
|
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
|
+
}}
|
|
486
523
|
onUpdateConversation={handleUpdateConversation}
|
|
487
|
-
sharedContentOpen={
|
|
524
|
+
sharedContentOpen={detailsOpen}
|
|
488
525
|
title={selectedTitle}
|
|
489
526
|
/>
|
|
490
527
|
|
|
@@ -565,7 +602,11 @@ export function ChatWorkspace({
|
|
|
565
602
|
: undefined
|
|
566
603
|
}
|
|
567
604
|
currentUserId={currentUserId}
|
|
605
|
+
enableRootIntegrations={enableRootIntegrations}
|
|
568
606
|
onCreated={handleCreated}
|
|
607
|
+
onIntegrationCreated={(conversationId) =>
|
|
608
|
+
selectConversation(conversationId, 'agent')
|
|
609
|
+
}
|
|
569
610
|
onOpenChange={setCreateOpen}
|
|
570
611
|
open={createOpen}
|
|
571
612
|
wsId={wsId}
|
|
@@ -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
|
+
}
|