@lobehub/chat 1.76.1 → 1.77.1
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 +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/common.json +13 -2
- package/locales/ar/error.json +10 -0
- package/locales/ar/models.json +9 -6
- package/locales/ar/setting.json +28 -0
- package/locales/bg-BG/common.json +13 -2
- package/locales/bg-BG/error.json +10 -0
- package/locales/bg-BG/models.json +9 -6
- package/locales/bg-BG/setting.json +28 -0
- package/locales/de-DE/common.json +13 -2
- package/locales/de-DE/error.json +10 -0
- package/locales/de-DE/models.json +9 -6
- package/locales/de-DE/setting.json +28 -0
- package/locales/en-US/common.json +13 -2
- package/locales/en-US/error.json +10 -0
- package/locales/en-US/models.json +9 -6
- package/locales/en-US/setting.json +28 -0
- package/locales/es-ES/common.json +13 -2
- package/locales/es-ES/error.json +10 -0
- package/locales/es-ES/models.json +9 -6
- package/locales/es-ES/setting.json +28 -0
- package/locales/fa-IR/common.json +13 -2
- package/locales/fa-IR/error.json +10 -0
- package/locales/fa-IR/models.json +9 -6
- package/locales/fa-IR/setting.json +28 -0
- package/locales/fr-FR/common.json +13 -2
- package/locales/fr-FR/error.json +10 -0
- package/locales/fr-FR/models.json +9 -6
- package/locales/fr-FR/setting.json +28 -0
- package/locales/it-IT/common.json +13 -2
- package/locales/it-IT/error.json +10 -0
- package/locales/it-IT/models.json +9 -6
- package/locales/it-IT/setting.json +28 -0
- package/locales/ja-JP/common.json +13 -2
- package/locales/ja-JP/error.json +10 -0
- package/locales/ja-JP/models.json +9 -6
- package/locales/ja-JP/setting.json +28 -0
- package/locales/ko-KR/common.json +13 -2
- package/locales/ko-KR/error.json +10 -0
- package/locales/ko-KR/models.json +9 -6
- package/locales/ko-KR/setting.json +28 -0
- package/locales/nl-NL/common.json +13 -2
- package/locales/nl-NL/error.json +10 -0
- package/locales/nl-NL/models.json +9 -6
- package/locales/nl-NL/setting.json +28 -0
- package/locales/pl-PL/common.json +13 -2
- package/locales/pl-PL/error.json +10 -0
- package/locales/pl-PL/models.json +9 -6
- package/locales/pl-PL/setting.json +28 -0
- package/locales/pt-BR/common.json +13 -2
- package/locales/pt-BR/error.json +10 -0
- package/locales/pt-BR/models.json +9 -6
- package/locales/pt-BR/setting.json +28 -0
- package/locales/ru-RU/common.json +13 -2
- package/locales/ru-RU/error.json +10 -0
- package/locales/ru-RU/models.json +9 -6
- package/locales/ru-RU/setting.json +28 -0
- package/locales/tr-TR/common.json +13 -2
- package/locales/tr-TR/error.json +10 -0
- package/locales/tr-TR/models.json +9 -6
- package/locales/tr-TR/setting.json +28 -0
- package/locales/vi-VN/common.json +13 -2
- package/locales/vi-VN/error.json +10 -0
- package/locales/vi-VN/models.json +9 -6
- package/locales/vi-VN/setting.json +28 -0
- package/locales/zh-CN/common.json +13 -2
- package/locales/zh-CN/error.json +10 -0
- package/locales/zh-CN/models.json +10 -7
- package/locales/zh-CN/setting.json +28 -0
- package/locales/zh-TW/common.json +13 -2
- package/locales/zh-TW/error.json +10 -0
- package/locales/zh-TW/models.json +9 -6
- package/locales/zh-TW/setting.json +28 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/(mobile)/me/data/features/Category.tsx +2 -2
- package/src/app/[variants]/(main)/chat/features/Migration/UpgradeButton.tsx +2 -1
- package/src/app/[variants]/(main)/settings/common/features/Common.tsx +0 -44
- package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +40 -14
- package/src/app/[variants]/(main)/settings/storage/Advanced.tsx +108 -0
- package/src/app/[variants]/(main)/settings/storage/IndexedDBStorage.tsx +55 -0
- package/src/app/[variants]/(main)/settings/storage/page.tsx +17 -0
- package/src/components/GroupIcon/index.tsx +25 -0
- package/src/components/IndexCard/index.tsx +143 -0
- package/src/components/ProgressItem/index.tsx +75 -0
- package/src/database/models/__tests__/session.test.ts +21 -0
- package/src/database/repositories/dataExporter/index.test.ts +330 -0
- package/src/database/repositories/dataExporter/index.ts +216 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/agents.json +65 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/agentsToSessions.json +541 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/topic.json +269 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/userSettings.json +18 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/with-client-id.json +778 -0
- package/src/database/repositories/dataImporter/__tests__/index.test.ts +120 -880
- package/src/database/repositories/dataImporter/deprecated/__tests__/index.test.ts +940 -0
- package/src/database/repositories/dataImporter/deprecated/index.ts +326 -0
- package/src/database/repositories/dataImporter/index.ts +684 -289
- package/src/database/server/models/session.ts +85 -9
- package/src/features/DataImporter/ImportDetail.tsx +203 -0
- package/src/features/DataImporter/SuccessResult.tsx +22 -6
- package/src/features/DataImporter/_deprecated.ts +43 -0
- package/src/features/DataImporter/config.ts +21 -0
- package/src/features/DataImporter/index.tsx +112 -31
- package/src/features/DevPanel/PostgresViewer/DataTable/index.tsx +6 -0
- package/src/features/User/UserPanel/useMenu.tsx +1 -36
- package/src/features/User/__tests__/useMenu.test.tsx +0 -2
- package/src/locales/default/common.ts +12 -1
- package/src/locales/default/error.ts +10 -0
- package/src/locales/default/setting.ts +28 -0
- package/src/server/routers/lambda/exporter.ts +25 -0
- package/src/server/routers/lambda/importer.ts +19 -3
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/services/config.ts +80 -135
- package/src/services/export/_deprecated.ts +155 -0
- package/src/services/export/client.ts +15 -0
- package/src/services/export/index.ts +6 -0
- package/src/services/export/server.ts +9 -0
- package/src/services/export/type.ts +5 -0
- package/src/services/import/_deprecated.ts +42 -1
- package/src/services/import/client.test.ts +1 -1
- package/src/services/import/client.ts +30 -1
- package/src/services/import/server.ts +70 -2
- package/src/services/import/type.ts +10 -0
- package/src/store/global/initialState.ts +1 -0
- package/src/types/export.ts +11 -0
- package/src/types/exportConfig.ts +2 -0
- package/src/types/importer.ts +15 -0
- package/src/utils/client/exportFile.ts +21 -0
- package/vitest.config.ts +1 -1
- package/src/utils/config.ts +0 -109
- /package/src/database/repositories/dataImporter/{__tests__ → deprecated/__tests__}/fixtures/messages.json +0 -0
@@ -279,26 +279,102 @@ export class SessionModel {
|
|
279
279
|
// **************** Delete *************** //
|
280
280
|
|
281
281
|
/**
|
282
|
-
* Delete a session
|
282
|
+
* Delete a session and its associated agent data if no longer referenced.
|
283
283
|
*/
|
284
284
|
delete = async (id: string) => {
|
285
|
-
return this.db
|
286
|
-
|
287
|
-
|
285
|
+
return this.db.transaction(async (trx) => {
|
286
|
+
// First get the agent IDs associated with this session
|
287
|
+
const links = await trx
|
288
|
+
.select({ agentId: agentsToSessions.agentId })
|
289
|
+
.from(agentsToSessions)
|
290
|
+
.where(and(eq(agentsToSessions.sessionId, id), eq(agentsToSessions.userId, this.userId)));
|
291
|
+
|
292
|
+
const agentIds = links.map((link) => link.agentId);
|
293
|
+
|
294
|
+
// Delete links in agentsToSessions
|
295
|
+
await trx
|
296
|
+
.delete(agentsToSessions)
|
297
|
+
.where(and(eq(agentsToSessions.sessionId, id), eq(agentsToSessions.userId, this.userId)));
|
298
|
+
|
299
|
+
// Delete the session
|
300
|
+
const result = await trx
|
301
|
+
.delete(sessions)
|
302
|
+
.where(and(eq(sessions.id, id), eq(sessions.userId, this.userId)));
|
303
|
+
|
304
|
+
// Delete orphaned agents
|
305
|
+
await this.clearOrphanAgent(agentIds, trx);
|
306
|
+
|
307
|
+
return result;
|
308
|
+
});
|
288
309
|
};
|
289
310
|
|
290
311
|
/**
|
291
|
-
* Batch delete sessions
|
312
|
+
* Batch delete sessions and their associated agent data if no longer referenced.
|
292
313
|
*/
|
293
314
|
batchDelete = async (ids: string[]) => {
|
294
|
-
return
|
295
|
-
|
296
|
-
|
315
|
+
if (ids.length === 0) return { count: 0 };
|
316
|
+
|
317
|
+
return this.db.transaction(async (trx) => {
|
318
|
+
// Get agent IDs associated with these sessions
|
319
|
+
const links = await trx
|
320
|
+
.select({ agentId: agentsToSessions.agentId })
|
321
|
+
.from(agentsToSessions)
|
322
|
+
.where(
|
323
|
+
and(inArray(agentsToSessions.sessionId, ids), eq(agentsToSessions.userId, this.userId)),
|
324
|
+
);
|
325
|
+
|
326
|
+
const agentIds = [...new Set(links.map((link) => link.agentId))];
|
327
|
+
|
328
|
+
// Delete links in agentsToSessions
|
329
|
+
await trx
|
330
|
+
.delete(agentsToSessions)
|
331
|
+
.where(
|
332
|
+
and(inArray(agentsToSessions.sessionId, ids), eq(agentsToSessions.userId, this.userId)),
|
333
|
+
);
|
334
|
+
|
335
|
+
// Delete the sessions
|
336
|
+
const result = await trx
|
337
|
+
.delete(sessions)
|
338
|
+
.where(and(inArray(sessions.id, ids), eq(sessions.userId, this.userId)));
|
339
|
+
|
340
|
+
// Delete orphaned agents
|
341
|
+
await this.clearOrphanAgent(agentIds, trx);
|
342
|
+
|
343
|
+
return result;
|
344
|
+
});
|
297
345
|
};
|
298
346
|
|
347
|
+
/**
|
348
|
+
* Delete all sessions and their associated agent data for this user.
|
349
|
+
*/
|
299
350
|
deleteAll = async () => {
|
300
|
-
return this.db.
|
351
|
+
return this.db.transaction(async (trx) => {
|
352
|
+
// Delete all agentsToSessions for this user
|
353
|
+
await trx.delete(agentsToSessions).where(eq(agentsToSessions.userId, this.userId));
|
354
|
+
|
355
|
+
// Delete all agents that were only used by this user's sessions
|
356
|
+
await trx.delete(agents).where(eq(agents.userId, this.userId));
|
357
|
+
|
358
|
+
// Delete all sessions for this user
|
359
|
+
return trx.delete(sessions).where(eq(sessions.userId, this.userId));
|
360
|
+
});
|
361
|
+
};
|
362
|
+
|
363
|
+
clearOrphanAgent = async (agentIds: string[], trx: any) => {
|
364
|
+
// Delete orphaned agents (those not linked to any other sessions)
|
365
|
+
for (const agentId of agentIds) {
|
366
|
+
const remaining = await trx
|
367
|
+
.select()
|
368
|
+
.from(agentsToSessions)
|
369
|
+
.where(eq(agentsToSessions.agentId, agentId))
|
370
|
+
.limit(1);
|
371
|
+
|
372
|
+
if (remaining.length === 0) {
|
373
|
+
await trx.delete(agents).where(and(eq(agents.id, agentId), eq(agents.userId, this.userId)));
|
374
|
+
}
|
375
|
+
}
|
301
376
|
};
|
377
|
+
|
302
378
|
// **************** Update *************** //
|
303
379
|
|
304
380
|
update = async (id: string, data: Partial<SessionItem>) => {
|
@@ -0,0 +1,203 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { Modal } from '@lobehub/ui';
|
4
|
+
import { Button, Table, Typography } from 'antd';
|
5
|
+
import { createStyles } from 'antd-style';
|
6
|
+
import { Info } from 'lucide-react';
|
7
|
+
import { useState } from 'react';
|
8
|
+
import { useTranslation } from 'react-i18next';
|
9
|
+
import { Flexbox } from 'react-layout-kit';
|
10
|
+
|
11
|
+
import { ImportPgDataStructure } from '@/types/export';
|
12
|
+
|
13
|
+
const { Text } = Typography;
|
14
|
+
|
15
|
+
const getNonEmptyTables = (data: ImportPgDataStructure) => {
|
16
|
+
const result = [];
|
17
|
+
|
18
|
+
for (const [key, value] of Object.entries(data.data)) {
|
19
|
+
if (Array.isArray(value) && value.length > 0) {
|
20
|
+
result.push({
|
21
|
+
count: value.length,
|
22
|
+
name: key,
|
23
|
+
});
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
return result;
|
28
|
+
};
|
29
|
+
|
30
|
+
const getTotalRecords = (tables: { count: number; name: string }[]): number => {
|
31
|
+
return tables.reduce((sum, table) => sum + table.count, 0);
|
32
|
+
};
|
33
|
+
|
34
|
+
const useStyles = createStyles(({ token, css }) => {
|
35
|
+
return {
|
36
|
+
duplicateAlert: css`
|
37
|
+
margin-block-start: ${token.marginMD}px;
|
38
|
+
padding: ${token.paddingMD}px;
|
39
|
+
border: 1px solid ${token.colorWarningBorder};
|
40
|
+
border-radius: ${token.borderRadiusLG}px;
|
41
|
+
|
42
|
+
background-color: ${token.colorWarningBg};
|
43
|
+
`,
|
44
|
+
duplicateDescription: css`
|
45
|
+
margin-block-start: ${token.marginXS}px;
|
46
|
+
font-size: ${token.fontSizeSM}px;
|
47
|
+
color: ${token.colorTextSecondary};
|
48
|
+
`,
|
49
|
+
duplicateOptions: css`
|
50
|
+
margin-block-start: ${token.marginSM}px;
|
51
|
+
`,
|
52
|
+
duplicateTag: css`
|
53
|
+
border-color: ${token.colorWarningBorder};
|
54
|
+
color: ${token.colorWarning};
|
55
|
+
background-color: ${token.colorWarningBg};
|
56
|
+
`,
|
57
|
+
hash: css`
|
58
|
+
font-family: ${token.fontFamilyCode};
|
59
|
+
font-size: 12px;
|
60
|
+
color: ${token.colorTextTertiary};
|
61
|
+
`,
|
62
|
+
infoIcon: css`
|
63
|
+
color: ${token.colorTextSecondary};
|
64
|
+
`,
|
65
|
+
modalContent: css`
|
66
|
+
padding-block: ${token.paddingMD}px;
|
67
|
+
padding-inline: 0;
|
68
|
+
`,
|
69
|
+
successIcon: css`
|
70
|
+
color: ${token.colorSuccess};
|
71
|
+
`,
|
72
|
+
tableContainer: css`
|
73
|
+
overflow: hidden;
|
74
|
+
border: 1px solid ${token.colorBorderSecondary};
|
75
|
+
border-radius: ${token.borderRadiusLG}px;
|
76
|
+
`,
|
77
|
+
tableName: css`
|
78
|
+
font-family: ${token.fontFamilyCode};
|
79
|
+
`,
|
80
|
+
warningIcon: css`
|
81
|
+
color: ${token.colorWarning};
|
82
|
+
`,
|
83
|
+
};
|
84
|
+
});
|
85
|
+
|
86
|
+
interface ImportPreviewModalProps {
|
87
|
+
importData: ImportPgDataStructure;
|
88
|
+
onCancel?: () => void;
|
89
|
+
onConfirm?: (overwriteExisting: boolean) => void;
|
90
|
+
onOpenChange: (open: boolean) => void;
|
91
|
+
open: boolean;
|
92
|
+
}
|
93
|
+
|
94
|
+
const ImportPreviewModal = ({
|
95
|
+
open = true,
|
96
|
+
onOpenChange = () => {},
|
97
|
+
onConfirm = () => {},
|
98
|
+
onCancel = () => {},
|
99
|
+
importData,
|
100
|
+
}: ImportPreviewModalProps) => {
|
101
|
+
const { t } = useTranslation('common');
|
102
|
+
const { styles } = useStyles();
|
103
|
+
const [duplicateAction] = useState<string>('skip');
|
104
|
+
const tables = getNonEmptyTables(importData);
|
105
|
+
const totalRecords = getTotalRecords(tables);
|
106
|
+
|
107
|
+
// 表格列定义
|
108
|
+
const columns = [
|
109
|
+
{
|
110
|
+
dataIndex: 'name',
|
111
|
+
key: 'name',
|
112
|
+
render: (text: string) => <div className={styles.tableName}>{text}</div>,
|
113
|
+
title: t('importPreview.tables.name'),
|
114
|
+
},
|
115
|
+
{
|
116
|
+
dataIndex: 'count',
|
117
|
+
key: 'count',
|
118
|
+
title: t('importPreview.tables.count'),
|
119
|
+
},
|
120
|
+
];
|
121
|
+
|
122
|
+
const handleConfirm = () => {
|
123
|
+
onConfirm(duplicateAction === 'overwrite');
|
124
|
+
onOpenChange(false);
|
125
|
+
};
|
126
|
+
|
127
|
+
return (
|
128
|
+
<Modal
|
129
|
+
footer={[
|
130
|
+
<Button
|
131
|
+
key="cancel"
|
132
|
+
onClick={() => {
|
133
|
+
onOpenChange(false);
|
134
|
+
onCancel();
|
135
|
+
}}
|
136
|
+
>
|
137
|
+
{t('cancel')}
|
138
|
+
</Button>,
|
139
|
+
<Button key="confirm" onClick={handleConfirm} type="primary">
|
140
|
+
{t('importPreview.confirmImport')}
|
141
|
+
</Button>,
|
142
|
+
]}
|
143
|
+
onCancel={() => onOpenChange(false)}
|
144
|
+
open={open}
|
145
|
+
title={t('importPreview.title')}
|
146
|
+
width={700}
|
147
|
+
>
|
148
|
+
<div className={styles.modalContent}>
|
149
|
+
<Flexbox gap={16}>
|
150
|
+
<Flexbox gap={4}>
|
151
|
+
<Flexbox align="center" horizontal justify="space-between" width="100%">
|
152
|
+
<Flexbox align="center" gap={8} horizontal>
|
153
|
+
<Info className={styles.infoIcon} size={16} />
|
154
|
+
<Text strong>{t('importPreview.totalRecords', { count: totalRecords })}</Text>
|
155
|
+
</Flexbox>
|
156
|
+
<Flexbox horizontal>
|
157
|
+
<Text type="secondary">
|
158
|
+
{t('importPreview.totalTables', { count: tables.length })}
|
159
|
+
</Text>
|
160
|
+
</Flexbox>
|
161
|
+
</Flexbox>
|
162
|
+
<Flexbox className={styles.hash} gap={4} horizontal>
|
163
|
+
Hash: <span>{importData.schemaHash}</span>
|
164
|
+
</Flexbox>
|
165
|
+
</Flexbox>
|
166
|
+
|
167
|
+
<div className={styles.tableContainer}>
|
168
|
+
<Table
|
169
|
+
columns={columns}
|
170
|
+
dataSource={tables}
|
171
|
+
pagination={false}
|
172
|
+
rowKey="name"
|
173
|
+
scroll={{ y: 350 }}
|
174
|
+
size="small"
|
175
|
+
/>
|
176
|
+
</div>
|
177
|
+
|
178
|
+
{/*<Flexbox>*/}
|
179
|
+
{/* 重复数据处理方式:*/}
|
180
|
+
{/* <div className={styles.duplicateOptions}>*/}
|
181
|
+
{/* <Radio.Group*/}
|
182
|
+
{/* onChange={(e) => setDuplicateAction(e.target.value)}*/}
|
183
|
+
{/* value={duplicateAction}*/}
|
184
|
+
{/* >*/}
|
185
|
+
{/* <Space>*/}
|
186
|
+
{/* <Radio value="skip">跳过</Radio>*/}
|
187
|
+
{/* <Radio value="overwrite">覆盖</Radio>*/}
|
188
|
+
{/* </Space>*/}
|
189
|
+
{/* </Radio.Group>*/}
|
190
|
+
{/* </div>*/}
|
191
|
+
{/* <div className={styles.duplicateDescription}>*/}
|
192
|
+
{/* {duplicateAction === 'skip'*/}
|
193
|
+
{/* ? '选择跳过将仅导入不重复的数据,保留现有数据不变。'*/}
|
194
|
+
{/* : '选择覆盖将使用导入数据替换系统中具有相同 ID 的现有记录。'}*/}
|
195
|
+
{/* </div>*/}
|
196
|
+
{/*</Flexbox>*/}
|
197
|
+
</Flexbox>
|
198
|
+
</div>
|
199
|
+
</Modal>
|
200
|
+
);
|
201
|
+
};
|
202
|
+
|
203
|
+
export default ImportPreviewModal;
|
@@ -2,17 +2,27 @@
|
|
2
2
|
|
3
3
|
import { Icon } from '@lobehub/ui';
|
4
4
|
import { Button, Result, Table } from 'antd';
|
5
|
+
import { createStyles } from 'antd-style';
|
5
6
|
import { CheckCircle } from 'lucide-react';
|
6
7
|
import React, { memo } from 'react';
|
7
8
|
import { useTranslation } from 'react-i18next';
|
8
9
|
import { Flexbox } from 'react-layout-kit';
|
9
10
|
|
11
|
+
const useStyles = createStyles(({ token, css }) => {
|
12
|
+
return {
|
13
|
+
zeroCell: css`
|
14
|
+
color: ${token.colorTextQuaternary};
|
15
|
+
`,
|
16
|
+
};
|
17
|
+
});
|
18
|
+
|
10
19
|
interface SuccessResultProps {
|
11
20
|
dataSource?: {
|
12
21
|
added: number;
|
13
22
|
error: number;
|
14
23
|
skips: number;
|
15
24
|
title: string;
|
25
|
+
updated: number;
|
16
26
|
}[];
|
17
27
|
duration: number;
|
18
28
|
onClickFinish?: () => void;
|
@@ -20,7 +30,11 @@ interface SuccessResultProps {
|
|
20
30
|
|
21
31
|
const SuccessResult = memo<SuccessResultProps>(({ duration, dataSource, onClickFinish }) => {
|
22
32
|
const { t } = useTranslation('common');
|
33
|
+
const { styles } = useStyles();
|
23
34
|
|
35
|
+
const cellRender = (text: string) => {
|
36
|
+
return text ? text : <span className={styles.zeroCell}>0</span>;
|
37
|
+
};
|
24
38
|
return (
|
25
39
|
<Result
|
26
40
|
extra={
|
@@ -30,24 +44,26 @@ const SuccessResult = memo<SuccessResultProps>(({ duration, dataSource, onClickF
|
|
30
44
|
}
|
31
45
|
icon={<Icon icon={CheckCircle} />}
|
32
46
|
status={'success'}
|
33
|
-
style={{ paddingBlock: 24 }}
|
47
|
+
style={{ paddingBlock: 24, paddingInline: 0 }}
|
34
48
|
subTitle={
|
35
49
|
// if there is no importData, means it's only import the settings
|
36
50
|
!dataSource ? (
|
37
51
|
t('importModal.finish.onlySettings')
|
38
52
|
) : (
|
39
|
-
<Flexbox gap={16} width={
|
53
|
+
<Flexbox gap={16} width={500}>
|
40
54
|
{t('importModal.finish.subTitle', { duration: (duration / 1000).toFixed(2) })}
|
41
55
|
<Table
|
42
56
|
bordered
|
43
57
|
columns={[
|
44
|
-
{ dataIndex: 'title', title: t('importModal.result.type') },
|
45
|
-
{ dataIndex: 'added', title: t('importModal.result.added') },
|
46
|
-
{ dataIndex: 'skips', title: t('importModal.result.skips') },
|
47
|
-
{ dataIndex: 'error', title: t('importModal.result.errors') },
|
58
|
+
{ dataIndex: 'title', render: cellRender, title: t('importModal.result.type') },
|
59
|
+
{ dataIndex: 'added', render: cellRender, title: t('importModal.result.added') },
|
60
|
+
{ dataIndex: 'skips', render: cellRender, title: t('importModal.result.skips') },
|
61
|
+
{ dataIndex: 'error', render: cellRender, title: t('importModal.result.errors') },
|
62
|
+
{ dataIndex: 'updated', render: cellRender, title: t('importModal.result.update') },
|
48
63
|
]}
|
49
64
|
dataSource={dataSource}
|
50
65
|
pagination={false}
|
66
|
+
rowKey={'title'}
|
51
67
|
size={'small'}
|
52
68
|
/>
|
53
69
|
</Flexbox>
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import { t } from 'i18next';
|
2
|
+
|
3
|
+
import { notification } from '@/components/AntdStaticMethods';
|
4
|
+
import { Migration } from '@/migrations';
|
5
|
+
import { ConfigFile } from '@/types/exportConfig';
|
6
|
+
|
7
|
+
/**
|
8
|
+
* V2 删除该方法
|
9
|
+
* 不再需要 Migration.migrate
|
10
|
+
* @deprecated
|
11
|
+
*/
|
12
|
+
export const importConfigFile = async (
|
13
|
+
file: File,
|
14
|
+
onConfigImport: (config: ConfigFile) => Promise<void>,
|
15
|
+
) => {
|
16
|
+
const text = await file.text();
|
17
|
+
|
18
|
+
try {
|
19
|
+
const config = JSON.parse(text);
|
20
|
+
|
21
|
+
// it means the config file is exported from a newer version
|
22
|
+
if ('schemaHash' in config) {
|
23
|
+
notification.error({
|
24
|
+
description: t('import.incompatible.description', { ns: 'error' }),
|
25
|
+
message: t('import.incompatible.title', { ns: 'error' }),
|
26
|
+
});
|
27
|
+
return;
|
28
|
+
}
|
29
|
+
|
30
|
+
const { state, version } = Migration.migrate(config);
|
31
|
+
|
32
|
+
await onConfigImport({ ...config, state, version });
|
33
|
+
} catch (error) {
|
34
|
+
console.error(error);
|
35
|
+
notification.error({
|
36
|
+
description: t('import.importConfigFile.description', {
|
37
|
+
ns: 'error',
|
38
|
+
reason: (error as any).message,
|
39
|
+
}),
|
40
|
+
message: t('import.importConfigFile.title', { ns: 'error' }),
|
41
|
+
});
|
42
|
+
}
|
43
|
+
};
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { t } from 'i18next';
|
2
|
+
|
3
|
+
import { notification } from '@/components/AntdStaticMethods';
|
4
|
+
import { ImportPgDataStructure } from '@/types/export';
|
5
|
+
|
6
|
+
export const parseConfigFile = async (file: File): Promise<ImportPgDataStructure | undefined> => {
|
7
|
+
const text = await file.text();
|
8
|
+
|
9
|
+
try {
|
10
|
+
return JSON.parse(text);
|
11
|
+
} catch (error) {
|
12
|
+
console.error(error);
|
13
|
+
notification.error({
|
14
|
+
description: t('import.importConfigFile.description', {
|
15
|
+
ns: 'error',
|
16
|
+
reason: (error as any).message,
|
17
|
+
}),
|
18
|
+
message: t('import.importConfigFile.title', { ns: 'error' }),
|
19
|
+
});
|
20
|
+
}
|
21
|
+
};
|
@@ -8,16 +8,21 @@ import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { Center } from 'react-layout-kit';
|
9
9
|
|
10
10
|
import DataStyleModal from '@/components/DataStyleModal';
|
11
|
-
import {
|
11
|
+
import { importService } from '@/services/import';
|
12
|
+
import { ImportResult, ImportResults } from '@/services/import/_deprecated';
|
12
13
|
import { useChatStore } from '@/store/chat';
|
13
14
|
import { useSessionStore } from '@/store/session';
|
14
|
-
import {
|
15
|
-
import {
|
15
|
+
import { ImportPgDataStructure } from '@/types/export';
|
16
|
+
import { ConfigFile } from '@/types/exportConfig';
|
17
|
+
import { ErrorShape, FileUploadState, ImportStage, OnImportCallbacks } from '@/types/importer';
|
16
18
|
|
17
19
|
import ImportError from './Error';
|
18
20
|
import { FileUploading } from './FileUploading';
|
21
|
+
import ImportPreviewModal from './ImportDetail';
|
19
22
|
import DataLoading from './Loading';
|
20
23
|
import SuccessResult from './SuccessResult';
|
24
|
+
import { importConfigFile } from './_deprecated';
|
25
|
+
import { parseConfigFile } from './config';
|
21
26
|
|
22
27
|
const useStyles = createStyles(({ css }) => ({
|
23
28
|
children: css`
|
@@ -50,13 +55,16 @@ const DataImporter = memo<DataImporterProps>(({ children, onFinishImport }) => {
|
|
50
55
|
|
51
56
|
const [fileUploadingState, setUploadingState] = useState<FileUploadState | undefined>();
|
52
57
|
const [importError, setImportError] = useState<ErrorShape | undefined>();
|
53
|
-
const [
|
58
|
+
const [importResults, setImportResults] = useState<ImportResults | undefined>();
|
59
|
+
const [showImportModal, setShowImportModal] = useState(false);
|
60
|
+
const [importPgData, setImportPgData] = useState<ImportPgDataStructure | undefined>(undefined);
|
54
61
|
|
55
62
|
const dataSource = useMemo(() => {
|
56
|
-
if (!
|
63
|
+
if (!importResults) return;
|
57
64
|
|
58
|
-
const { type, ...res } =
|
65
|
+
const { type, ...res } = importResults;
|
59
66
|
|
67
|
+
console.log(res);
|
60
68
|
if (type === 'settings') return;
|
61
69
|
|
62
70
|
return Object.entries(res)
|
@@ -65,15 +73,16 @@ const DataImporter = memo<DataImporterProps>(({ children, onFinishImport }) => {
|
|
65
73
|
added: value.added,
|
66
74
|
error: value.errors,
|
67
75
|
skips: value.skips,
|
68
|
-
title:
|
76
|
+
title: item,
|
77
|
+
updated: value.updated || 0,
|
69
78
|
}));
|
70
|
-
}, [
|
79
|
+
}, [importResults]);
|
71
80
|
|
72
81
|
const isFinished = importState === ImportStage.Success || importState === ImportStage.Error;
|
73
82
|
|
74
83
|
const closeModal = () => {
|
75
84
|
setImportState(ImportStage.Finished);
|
76
|
-
|
85
|
+
setImportResults(undefined);
|
77
86
|
setImportError(undefined);
|
78
87
|
setUploadingState(undefined);
|
79
88
|
|
@@ -114,7 +123,7 @@ const DataImporter = memo<DataImporterProps>(({ children, onFinishImport }) => {
|
|
114
123
|
|
115
124
|
case ImportStage.Success: {
|
116
125
|
return (
|
117
|
-
<Center gap={24} paddingInline={
|
126
|
+
<Center gap={24} paddingInline={16}>
|
118
127
|
<SuccessResult dataSource={dataSource} duration={duration} onClickFinish={closeModal} />
|
119
128
|
</Center>
|
120
129
|
);
|
@@ -139,35 +148,74 @@ const DataImporter = memo<DataImporterProps>(({ children, onFinishImport }) => {
|
|
139
148
|
icon={ImportIcon}
|
140
149
|
open={importState !== ImportStage.Start && importState !== ImportStage.Finished}
|
141
150
|
title={t('importModal.title')}
|
142
|
-
width={isFinished ?
|
151
|
+
width={isFinished ? 600 : 400}
|
143
152
|
>
|
144
153
|
{content}
|
145
154
|
</DataStyleModal>
|
146
155
|
<Upload
|
156
|
+
accept={'application/json'}
|
147
157
|
beforeUpload={async (file) => {
|
148
|
-
await
|
149
|
-
|
158
|
+
const config = await parseConfigFile(file);
|
159
|
+
if (!config) return false;
|
150
160
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
161
|
+
if (!('schemaHash' in config)) {
|
162
|
+
// TODO: remove in V2
|
163
|
+
await importConfigFile(file, async (config) => {
|
164
|
+
setImportState(ImportStage.Preparing);
|
165
|
+
console.log(config);
|
166
|
+
|
167
|
+
const importConfigState = async (
|
168
|
+
config: ConfigFile,
|
169
|
+
callbacks?: OnImportCallbacks,
|
170
|
+
): Promise<void> => {
|
171
|
+
if (config.exportType === 'settings') {
|
172
|
+
await importService.importSettings(config.state.settings);
|
173
|
+
callbacks?.onStageChange?.(ImportStage.Success);
|
174
|
+
return;
|
175
|
+
}
|
176
|
+
|
177
|
+
if (config.exportType === 'all') {
|
178
|
+
await importService.importSettings(config.state.settings);
|
179
|
+
}
|
180
|
+
|
181
|
+
await importService.importData(
|
182
|
+
{
|
183
|
+
messages: (config.state as any).messages || [],
|
184
|
+
sessionGroups: (config.state as any).sessionGroups || [],
|
185
|
+
sessions: (config.state as any).sessions || [],
|
186
|
+
topics: (config.state as any).topics || [],
|
187
|
+
version: config.version,
|
188
|
+
},
|
189
|
+
callbacks,
|
190
|
+
);
|
191
|
+
};
|
192
|
+
|
193
|
+
await importConfigState(config, {
|
194
|
+
onError: (error) => {
|
195
|
+
setImportError(error);
|
196
|
+
},
|
197
|
+
onFileUploading: (state) => {
|
198
|
+
setUploadingState(state);
|
199
|
+
},
|
200
|
+
onStageChange: (stage) => {
|
201
|
+
setImportState(stage);
|
202
|
+
},
|
203
|
+
onSuccess: (data, duration) => {
|
204
|
+
if (data) setImportResults(data);
|
205
|
+
setDuration(duration);
|
206
|
+
},
|
207
|
+
});
|
208
|
+
|
209
|
+
await refreshSessions();
|
210
|
+
await refreshMessages();
|
211
|
+
await refreshTopics();
|
165
212
|
});
|
166
213
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
214
|
+
return false;
|
215
|
+
}
|
216
|
+
|
217
|
+
setImportPgData(config);
|
218
|
+
setShowImportModal(true);
|
171
219
|
|
172
220
|
return false;
|
173
221
|
}}
|
@@ -178,6 +226,39 @@ const DataImporter = memo<DataImporterProps>(({ children, onFinishImport }) => {
|
|
178
226
|
{/* a very hackable solution: add a pseudo before to have a large hot zone */}
|
179
227
|
<div className={styles.children}>{children}</div>
|
180
228
|
</Upload>
|
229
|
+
{importPgData && (
|
230
|
+
<ImportPreviewModal
|
231
|
+
importData={importPgData}
|
232
|
+
onConfirm={async (overwriteExisting) => {
|
233
|
+
setImportState(ImportStage.Preparing);
|
234
|
+
|
235
|
+
await importService.importPgData(importPgData, {
|
236
|
+
callbacks: {
|
237
|
+
onError: (error) => {
|
238
|
+
setImportError(error);
|
239
|
+
},
|
240
|
+
onFileUploading: (state) => {
|
241
|
+
setUploadingState(state);
|
242
|
+
},
|
243
|
+
onStageChange: (stage) => {
|
244
|
+
setImportState(stage);
|
245
|
+
},
|
246
|
+
onSuccess: (data, duration) => {
|
247
|
+
if (data) setImportResults(data);
|
248
|
+
setDuration(duration);
|
249
|
+
},
|
250
|
+
},
|
251
|
+
overwriteExisting,
|
252
|
+
});
|
253
|
+
|
254
|
+
await refreshSessions();
|
255
|
+
await refreshMessages();
|
256
|
+
await refreshTopics();
|
257
|
+
}}
|
258
|
+
onOpenChange={setShowImportModal}
|
259
|
+
open={showImportModal}
|
260
|
+
/>
|
261
|
+
)}
|
181
262
|
</>
|
182
263
|
);
|
183
264
|
});
|
@@ -5,6 +5,8 @@ import React from 'react';
|
|
5
5
|
import { Center, Flexbox } from 'react-layout-kit';
|
6
6
|
import { mutate } from 'swr';
|
7
7
|
|
8
|
+
import { exportService } from '@/services/export';
|
9
|
+
|
8
10
|
import Header from '../../features/Header';
|
9
11
|
import Table from '../../features/Table';
|
10
12
|
import { FETCH_TABLE_DATA_KEY, usePgTable, useTableColumns } from '../usePgTable';
|
@@ -39,6 +41,10 @@ const DataTable = ({ tableName }: DataTableProps) => {
|
|
39
41
|
},
|
40
42
|
{
|
41
43
|
icon: Download,
|
44
|
+
onClick: async () => {
|
45
|
+
const data = await exportService.exportData();
|
46
|
+
console.log(data);
|
47
|
+
},
|
42
48
|
title: 'Export',
|
43
49
|
},
|
44
50
|
{
|