From 5538cc1953db5957b07da19a506585e452b87d5b Mon Sep 17 00:00:00 2001 From: xinyan Date: Sun, 7 Dec 2025 00:12:45 +0800 Subject: [PATCH] modified: src/lib/components/layout/ImportChatsModal.svelte --- .../components/layout/ImportChatsModal.svelte | 357 ++++++++---------- 1 file changed, 158 insertions(+), 199 deletions(-) diff --git a/src/lib/components/layout/ImportChatsModal.svelte b/src/lib/components/layout/ImportChatsModal.svelte index a9e4ca7224..30ca84863b 100644 --- a/src/lib/components/layout/ImportChatsModal.svelte +++ b/src/lib/components/layout/ImportChatsModal.svelte @@ -2,8 +2,6 @@ import { toast } from 'svelte-sonner'; import Modal from '../common/Modal.svelte'; import Spinner from '../common/Spinner.svelte'; - import { extractChatsFromFile } from '$lib/utils/chatImport'; - import { getImportOrigin, convertOpenAIChats, convertDeepseekChats } from '$lib/utils'; export let show = false; export let onImport: (chats: any[]) => Promise; @@ -15,9 +13,15 @@ let errorMsg = ''; let successMsg = ''; let fileName = ''; + + // 对应 HTML 版本中的 rawData let rawChats: any[] = []; + // 对应 HTML 版本中的 selectedIndices let selectedIndices: Set = new Set(); + let fileInputEl: HTMLInputElement; + + // 重置状态 const resetState = () => { errorMsg = ''; successMsg = ''; @@ -25,93 +29,64 @@ rawChats = []; selectedIndices = new Set(); filterOpen = true; + if (fileInputEl) fileInputEl.value = ''; }; $: if (!show) { resetState(); } - const parseJsonOrJsonlText = async (file: File) => { - const text = await file.text(); - try { - const parsed = JSON.parse(text); - return Array.isArray(parsed) ? parsed : [parsed]; - } catch (jsonError) { - const lines = text - .split('\n') - .map((l) => l.trim()) - .filter((l) => l.length > 0); - - if (lines.length === 0) { - throw new Error('File is empty, nothing to parse'); - } - - try { - return lines.map((line) => JSON.parse(line)); - } catch (lineError) { - throw new Error('Plain text JSONL must contain one valid JSON object per line'); - } - } - }; - - const normalizeChats = (chats: any) => { - if (Array.isArray(chats)) return chats; - - if (chats && typeof chats === 'object') { - if (Array.isArray((chats as any).conversations)) { - return (chats as any).conversations; - } - } - - throw new Error('File content must be a JSON array of chats'); - }; - + // 核心逻辑:读取并解析 JSON 数组 const handleFiles = async (files: FileList | File[]) => { if (!files || files.length === 0) return; const file = files[0]; + loading = true; errorMsg = ''; successMsg = ''; fileName = file.name; + rawChats = []; + selectedIndices = new Set(); + try { - const ext = file.name.split('.').pop()?.toLowerCase(); - let chats: any = null; + const text = await file.text(); + let parsed: any; - if (ext === 'txt' || ext === 'jsonl') { - chats = await parseJsonOrJsonlText(file); - } else { - chats = await extractChatsFromFile(file); + try { + parsed = JSON.parse(text); + } catch (e) { + throw new Error('JSON 解析失败,请检查文件格式'); } - chats = normalizeChats(chats); - - if (chats.length === 0) { - throw new Error('File contained zero chat records'); + // 校验格式:必须是数组 [{}, {}] + if (!Array.isArray(parsed)) { + throw new Error('文件格式错误:JSON 根节点必须是数组 `[...]`'); } - const origin = getImportOrigin(chats); - - if (origin === 'openai') { - chats = convertOpenAIChats(chats); - } else if (origin === 'deepseek') { - chats = convertDeepseekChats(chats); + if (parsed.length === 0) { + throw new Error('JSON 数组为空'); } - rawChats = chats; - selectedIndices = new Set(rawChats.map((_, idx) => idx)); + rawChats = parsed; + successMsg = `解析成功,共 ${rawChats.length} 条记录`; + + // 默认不全选,或者全选,取决于你的偏好。这里保持原有逻辑(不选或全选) + // 这里改为:解析后默认显示列表,但不选中(等待用户操作),或者全选 + // 之前的 HTML 逻辑是空的,这里为了方便用户,可以默认不选,或者全选。 + // 让我们默认不选,让用户决定。 + selectedIndices = new Set(); filterOpen = true; - successMsg = '解析成功'; + } catch (error) { console.error(error); errorMsg = error instanceof Error ? error.message : `${error}`; - successMsg = ''; rawChats = []; - selectedIndices = new Set(); } finally { loading = false; } }; + // 行选择切换 const toggleRow = (idx: number) => { const next = new Set(selectedIndices); if (next.has(idx)) { @@ -122,39 +97,48 @@ selectedIndices = next; }; + // 全选/取消全选 const toggleSelectAll = (checked: boolean) => { selectedIndices = checked ? new Set(rawChats.map((_, idx) => idx)) : new Set(); }; + // 导出并导入 (Export & Import) const confirmImport = async () => { if (!rawChats.length) { - toast.error('Please upload a chat history file first'); + toast.error('请先上传文件'); return; } - const chatsToImport = - selectedIndices.size > 0 ? rawChats.filter((_, idx) => selectedIndices.has(idx)) : rawChats; - if (!chatsToImport.length) { - toast.error('No records selected'); + if (selectedIndices.size === 0) { + toast.error('未选择任何记录'); return; } + // 筛选数据 + const chatsToImport = rawChats.filter((_, idx) => selectedIndices.has(idx)); + try { importing = true; - const jsonlString = chatsToImport.map((item) => JSON.stringify(item)).join('\n'); - const blob = new Blob([jsonlString], { type: 'application/json' }); + + // 1. 生成并下载 JSON 文件 (对应 HTML 工具的导出功能) + // 使用 null, 2 进行美化输出 + const jsonString = JSON.stringify(chatsToImport, null, 2); + const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `filtered_chats_${new Date().toISOString().slice(0, 10)}.jsonl`; + a.download = `filtered_chats_${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); + toast.success(`已导出 ${chatsToImport.length} 条记录`); + + // 2. 执行回调,将数据导入应用 await onImport(chatsToImport); show = false; - toast.success('Starting import for the filtered chats'); + } catch (error) { console.error(error); toast.error(error instanceof Error ? error.message : `${error}`); @@ -163,29 +147,15 @@ } }; + // 辅助显示函数 (对应 HTML 中的 renderTable) const displayRows = () => rawChats.map((chat, idx) => { - const meta = chat?.meta ?? chat?.chat?.meta ?? chat; - const title = - meta?.title ?? - chat?.title ?? - chat?.chat?.title ?? - meta?.subject ?? - 'Untitled chat'; - const date = - meta?.inserted_at ?? - meta?.created_at ?? - meta?.updated_at ?? - chat?.inserted_at ?? - chat?.created_at ?? - chat?.updated_at ?? - '-'; - + // 宽容处理字段 + const title = chat.title || '<无标题>'; + const date = chat.inserted_at || '-'; return { idx, title, date }; }); - let fileInputEl: HTMLInputElement; - const handleFileInputChange = (event: Event) => { const input = event.currentTarget as HTMLInputElement; handleFiles(input.files ?? []); @@ -201,42 +171,27 @@
-
Chat Import Center
+
+ + Chat Filter & Import +
- Upload your exported history, filter the records you need, then import. + 导入 JSON 数组,筛选所需的聊天记录,自动下载并导入。
-
-
1. Prepare file
-
-

- Supported formats: JSON (.json), JSONL (.jsonl/.txt), or - OpenAI ZIP export (auto-converted). -

-

Large exports can be filtered below to speed up the import.

-
-
-
-
2. Filter records
- +
1. 读取文件 (JSON Array)
-
- {fileName ? `Selected: ${fileName}` : 'Drag a file here or click to upload'} +
+ {fileName ? `Current: ${fileName}` : '拖入 .json 文件'}
+
- Supports .json / .jsonl / .txt / .zip exports + 格式要求: [{...}, {...}]
+ {#if loading} -
+
- Parsing file... + 正在解析...
{/if} {#if errorMsg} -
{errorMsg}
+
{errorMsg}
{/if} {#if successMsg} -
{successMsg}
+
{successMsg}
{/if}
+
+ +
+
+
+ 2. 筛选记录 +
+ + {#if rawChats.length > 0} +
+ Total: {rawChats.length} | Selected: {selectedIndices.size} + +
+ {/if} +
{#if filterOpen} -
-
-
- Total: {rawChats.length} | Selected: {selectedIndices.size} - {selectedIndices.size === 0 && rawChats.length > 0 - ? ' (none selected will import all)' - : ''} -
- -
- -
- - +
+
+ + + + + + + + + {#if !rawChats.length} - - - + - - - {#if !rawChats.length} - - + + + - {:else} - {#each displayRows() as row} - - - - - - {/each} - {/if} - -
#TitleInserted At
PickTitle / SummaryTimestamp + 暂无数据,请先导入文件 +
- Upload a file to see filterable records + {:else} + {#each displayRows() as row (row.idx)} +
+ toggleRow(row.idx)} + /> + +
+ {row.title} +
+
+ {row.date}
- toggleRow(row.idx)} - /> - -
- {row.title} -
-
- {row.date} -
-
+ {/each} + {/if} + +
{/if}
-
-
3. Import
-
- Confirmed records will be imported into your current account. -
-
- - -
+
+ +
- + \ No newline at end of file