@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 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.2",
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.1.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.3.0",
90
- "@tuturuuu/supabase": "0.3.2",
89
+ "@tuturuuu/internal-api": "0.4.0",
90
+ "@tuturuuu/supabase": "0.3.3",
91
91
  "@tuturuuu/trigger": "0.2.0",
92
- "@tuturuuu/utils": "0.4.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.5.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: type === 'ai' ? { source: 'personal-ai-chat' } : {},
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: () => null,
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
- const nextParams = new URLSearchParams(searchParams.toString());
124
- nextParams.set('conversationId', selectedConversationId);
125
- const nextQuery = nextParams.toString();
126
- window.history.replaceState(
127
- null,
128
- '',
129
- nextQuery ? `${pathname}?${nextQuery}` : pathname
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(conversationId: string) {
141
- const nextParams = new URLSearchParams(searchParams.toString());
142
- nextParams.set('conversationId', conversationId);
143
- const nextQuery = nextParams.toString();
144
- window.history.replaceState(
145
- null,
146
- '',
147
- nextQuery ? `${pathname}?${nextQuery}` : pathname
148
- );
149
- localStorage.setItem(selectionStorageKey, conversationId);
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(['direct-1', 'group-1', 'ai-chat-1', 'personal-ai-1']);
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
- (selectedConversationId && conversationIds.has(selectedConversationId)
130
- ? conversations.find((item) => item.id === selectedConversationId)
131
- : null) ??
132
- conversations[0] ??
133
- null,
134
- [conversationIds, conversations, selectedConversationId]
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 detailsOpen = Boolean(sharedContentOpen && activeConversationId);
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
- const nextParams = new URLSearchParams(searchParams.toString());
238
- nextParams.set('conversationId', activeConversationId);
239
- const nextQuery = nextParams.toString();
240
- window.history.replaceState(
241
- null,
242
- '',
243
- nextQuery ? `${pathname}?${nextQuery}` : pathname
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(conversationId: string) {
314
- const nextParams = new URLSearchParams(searchParams.toString());
315
- nextParams.set('conversationId', conversationId);
316
- const nextQuery = nextParams.toString();
317
- window.history.replaceState(
318
- null,
319
- '',
320
- nextQuery ? `${pathname}?${nextQuery}` : pathname
321
- );
322
- if (selectionStorageKey) {
323
- localStorage.setItem(selectionStorageKey, conversationId);
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
- const nextParams = new URLSearchParams(searchParams.toString());
362
- nextParams.delete('conversationId');
363
- const nextQuery = nextParams.toString();
364
- window.history.replaceState(
365
- null,
366
- '',
367
- nextQuery ? `${pathname}?${nextQuery}` : pathname
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
- setSharedContentOpen((current) => !current)
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={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
+ }