@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.
Files changed (131) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/common.json +13 -2
  4. package/locales/ar/error.json +10 -0
  5. package/locales/ar/models.json +9 -6
  6. package/locales/ar/setting.json +28 -0
  7. package/locales/bg-BG/common.json +13 -2
  8. package/locales/bg-BG/error.json +10 -0
  9. package/locales/bg-BG/models.json +9 -6
  10. package/locales/bg-BG/setting.json +28 -0
  11. package/locales/de-DE/common.json +13 -2
  12. package/locales/de-DE/error.json +10 -0
  13. package/locales/de-DE/models.json +9 -6
  14. package/locales/de-DE/setting.json +28 -0
  15. package/locales/en-US/common.json +13 -2
  16. package/locales/en-US/error.json +10 -0
  17. package/locales/en-US/models.json +9 -6
  18. package/locales/en-US/setting.json +28 -0
  19. package/locales/es-ES/common.json +13 -2
  20. package/locales/es-ES/error.json +10 -0
  21. package/locales/es-ES/models.json +9 -6
  22. package/locales/es-ES/setting.json +28 -0
  23. package/locales/fa-IR/common.json +13 -2
  24. package/locales/fa-IR/error.json +10 -0
  25. package/locales/fa-IR/models.json +9 -6
  26. package/locales/fa-IR/setting.json +28 -0
  27. package/locales/fr-FR/common.json +13 -2
  28. package/locales/fr-FR/error.json +10 -0
  29. package/locales/fr-FR/models.json +9 -6
  30. package/locales/fr-FR/setting.json +28 -0
  31. package/locales/it-IT/common.json +13 -2
  32. package/locales/it-IT/error.json +10 -0
  33. package/locales/it-IT/models.json +9 -6
  34. package/locales/it-IT/setting.json +28 -0
  35. package/locales/ja-JP/common.json +13 -2
  36. package/locales/ja-JP/error.json +10 -0
  37. package/locales/ja-JP/models.json +9 -6
  38. package/locales/ja-JP/setting.json +28 -0
  39. package/locales/ko-KR/common.json +13 -2
  40. package/locales/ko-KR/error.json +10 -0
  41. package/locales/ko-KR/models.json +9 -6
  42. package/locales/ko-KR/setting.json +28 -0
  43. package/locales/nl-NL/common.json +13 -2
  44. package/locales/nl-NL/error.json +10 -0
  45. package/locales/nl-NL/models.json +9 -6
  46. package/locales/nl-NL/setting.json +28 -0
  47. package/locales/pl-PL/common.json +13 -2
  48. package/locales/pl-PL/error.json +10 -0
  49. package/locales/pl-PL/models.json +9 -6
  50. package/locales/pl-PL/setting.json +28 -0
  51. package/locales/pt-BR/common.json +13 -2
  52. package/locales/pt-BR/error.json +10 -0
  53. package/locales/pt-BR/models.json +9 -6
  54. package/locales/pt-BR/setting.json +28 -0
  55. package/locales/ru-RU/common.json +13 -2
  56. package/locales/ru-RU/error.json +10 -0
  57. package/locales/ru-RU/models.json +9 -6
  58. package/locales/ru-RU/setting.json +28 -0
  59. package/locales/tr-TR/common.json +13 -2
  60. package/locales/tr-TR/error.json +10 -0
  61. package/locales/tr-TR/models.json +9 -6
  62. package/locales/tr-TR/setting.json +28 -0
  63. package/locales/vi-VN/common.json +13 -2
  64. package/locales/vi-VN/error.json +10 -0
  65. package/locales/vi-VN/models.json +9 -6
  66. package/locales/vi-VN/setting.json +28 -0
  67. package/locales/zh-CN/common.json +13 -2
  68. package/locales/zh-CN/error.json +10 -0
  69. package/locales/zh-CN/models.json +10 -7
  70. package/locales/zh-CN/setting.json +28 -0
  71. package/locales/zh-TW/common.json +13 -2
  72. package/locales/zh-TW/error.json +10 -0
  73. package/locales/zh-TW/models.json +9 -6
  74. package/locales/zh-TW/setting.json +28 -0
  75. package/package.json +1 -1
  76. package/src/app/[variants]/(main)/(mobile)/me/data/features/Category.tsx +2 -2
  77. package/src/app/[variants]/(main)/chat/features/Migration/UpgradeButton.tsx +2 -1
  78. package/src/app/[variants]/(main)/settings/common/features/Common.tsx +0 -44
  79. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +40 -14
  80. package/src/app/[variants]/(main)/settings/storage/Advanced.tsx +108 -0
  81. package/src/app/[variants]/(main)/settings/storage/IndexedDBStorage.tsx +55 -0
  82. package/src/app/[variants]/(main)/settings/storage/page.tsx +17 -0
  83. package/src/components/GroupIcon/index.tsx +25 -0
  84. package/src/components/IndexCard/index.tsx +143 -0
  85. package/src/components/ProgressItem/index.tsx +75 -0
  86. package/src/database/models/__tests__/session.test.ts +21 -0
  87. package/src/database/repositories/dataExporter/index.test.ts +330 -0
  88. package/src/database/repositories/dataExporter/index.ts +216 -0
  89. package/src/database/repositories/dataImporter/__tests__/fixtures/agents.json +65 -0
  90. package/src/database/repositories/dataImporter/__tests__/fixtures/agentsToSessions.json +541 -0
  91. package/src/database/repositories/dataImporter/__tests__/fixtures/topic.json +269 -0
  92. package/src/database/repositories/dataImporter/__tests__/fixtures/userSettings.json +18 -0
  93. package/src/database/repositories/dataImporter/__tests__/fixtures/with-client-id.json +778 -0
  94. package/src/database/repositories/dataImporter/__tests__/index.test.ts +120 -880
  95. package/src/database/repositories/dataImporter/deprecated/__tests__/index.test.ts +940 -0
  96. package/src/database/repositories/dataImporter/deprecated/index.ts +326 -0
  97. package/src/database/repositories/dataImporter/index.ts +684 -289
  98. package/src/database/server/models/session.ts +85 -9
  99. package/src/features/DataImporter/ImportDetail.tsx +203 -0
  100. package/src/features/DataImporter/SuccessResult.tsx +22 -6
  101. package/src/features/DataImporter/_deprecated.ts +43 -0
  102. package/src/features/DataImporter/config.ts +21 -0
  103. package/src/features/DataImporter/index.tsx +112 -31
  104. package/src/features/DevPanel/PostgresViewer/DataTable/index.tsx +6 -0
  105. package/src/features/User/UserPanel/useMenu.tsx +1 -36
  106. package/src/features/User/__tests__/useMenu.test.tsx +0 -2
  107. package/src/locales/default/common.ts +12 -1
  108. package/src/locales/default/error.ts +10 -0
  109. package/src/locales/default/setting.ts +28 -0
  110. package/src/server/routers/lambda/exporter.ts +25 -0
  111. package/src/server/routers/lambda/importer.ts +19 -3
  112. package/src/server/routers/lambda/index.ts +2 -0
  113. package/src/services/config.ts +80 -135
  114. package/src/services/export/_deprecated.ts +155 -0
  115. package/src/services/export/client.ts +15 -0
  116. package/src/services/export/index.ts +6 -0
  117. package/src/services/export/server.ts +9 -0
  118. package/src/services/export/type.ts +5 -0
  119. package/src/services/import/_deprecated.ts +42 -1
  120. package/src/services/import/client.test.ts +1 -1
  121. package/src/services/import/client.ts +30 -1
  122. package/src/services/import/server.ts +70 -2
  123. package/src/services/import/type.ts +10 -0
  124. package/src/store/global/initialState.ts +1 -0
  125. package/src/types/export.ts +11 -0
  126. package/src/types/exportConfig.ts +2 -0
  127. package/src/types/importer.ts +15 -0
  128. package/src/utils/client/exportFile.ts +21 -0
  129. package/vitest.config.ts +1 -1
  130. package/src/utils/config.ts +0 -109
  131. /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, also delete all messages and topics associated with it.
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
- .delete(sessions)
287
- .where(and(eq(sessions.id, id), eq(sessions.userId, this.userId)));
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, also delete all messages and topics associated with them.
312
+ * Batch delete sessions and their associated agent data if no longer referenced.
292
313
  */
293
314
  batchDelete = async (ids: string[]) => {
294
- return this.db
295
- .delete(sessions)
296
- .where(and(inArray(sessions.id, ids), eq(sessions.userId, this.userId)));
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.delete(sessions).where(eq(sessions.userId, this.userId));
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={400}>
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 { ImportResult, ImportResults, configService } from '@/services/config';
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 { ErrorShape, FileUploadState, ImportStage } from '@/types/importer';
15
- import { importConfigFile } from '@/utils/config';
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 [importData, setImportData] = useState<ImportResults | undefined>();
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 (!importData) return;
63
+ if (!importResults) return;
57
64
 
58
- const { type, ...res } = importData;
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: t(`importModal.result.${item as keyof ImportResults}`),
76
+ title: item,
77
+ updated: value.updated || 0,
69
78
  }));
70
- }, [importData]);
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
- setImportData(undefined);
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={40}>
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 ? 500 : 400}
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 importConfigFile(file, async (config) => {
149
- setImportState(ImportStage.Preparing);
158
+ const config = await parseConfigFile(file);
159
+ if (!config) return false;
150
160
 
151
- await configService.importConfigState(config, {
152
- onError: (error) => {
153
- setImportError(error);
154
- },
155
- onFileUploading: (state) => {
156
- setUploadingState(state);
157
- },
158
- onStageChange: (stage) => {
159
- setImportState(stage);
160
- },
161
- onSuccess: (data, duration) => {
162
- if (data) setImportData(data);
163
- setDuration(duration);
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
- await refreshSessions();
168
- await refreshMessages();
169
- await refreshTopics();
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
  {