diff --git a/packages/userscript/src/constants.ts b/packages/userscript/src/constants.ts index 274759e..c81f94a 100644 --- a/packages/userscript/src/constants.ts +++ b/packages/userscript/src/constants.ts @@ -10,6 +10,7 @@ export const apiUrl = API_MAPPING[baseUrl] export const LEGACY_KEY_FILENAME_FORMAT = 'exporter-format' export const KEY_LANGUAGE = 'exporter:language' export const KEY_FILENAME_FORMAT = 'exporter:filename_format' +export const KEY_OFFICIAL_JSON_FORMAT = 'exporter:official_json_format' export const KEY_TIMESTAMP_ENABLED = 'exporter:enable_timestamp' export const KEY_TIMESTAMP_24H = 'exporter:timestamp_24h' export const KEY_TIMESTAMP_MARKDOWN = 'exporter:timestamp_markdown' diff --git a/packages/userscript/src/exporter/json.ts b/packages/userscript/src/exporter/json.ts index 8a6d2fe..aa1e98d 100644 --- a/packages/userscript/src/exporter/json.ts +++ b/packages/userscript/src/exporter/json.ts @@ -5,7 +5,7 @@ import { checkIfConversationStarted, getConversationChoice } from '../page' import { downloadFile, getFileNameWithFormat } from '../utils/download' import type { ApiConversationWithId } from '../api' -export async function exportToJson(fileNameFormat: string) { +export async function exportToJson(fileNameFormat: string, options: { officialFormat: boolean }) { if (!checkIfConversationStarted()) { alert(i18n.t('Please start a conversation first')) return false @@ -17,12 +17,22 @@ export async function exportToJson(fileNameFormat: string) { const conversation = processConversation(rawConversation, conversationChoices) const fileName = getFileNameWithFormat(fileNameFormat, 'json', { title: conversation.title, chatId }) - const content = conversationToJson(rawConversation) + /** + * The official format is just an array of the API response. + */ + const content = conversationToJson(options.officialFormat ? [rawConversation] : rawConversation) downloadFile(fileName, 'application/json', content) return true } +export async function exportAllToOfficialJson(_fileNameFormat: string, apiConversations: ApiConversationWithId[]) { + const content = conversationToJson(apiConversations) + downloadFile('chatgpt-export.json', 'application/json', content) + + return true +} + export async function exportAllToJson(fileNameFormat: string, apiConversations: ApiConversationWithId[]) { const zip = new JSZip() const filenameMap = new Map() @@ -61,6 +71,6 @@ export async function exportAllToJson(fileNameFormat: string, apiConversations: return true } -function conversationToJson(conversation: ApiConversationWithId) { +function conversationToJson(conversation: ApiConversationWithId | ApiConversationWithId[]) { return JSON.stringify(conversation) } diff --git a/packages/userscript/src/locales/en.json b/packages/userscript/src/locales/en.json index b23917b..ca5150b 100644 --- a/packages/userscript/src/locales/en.json +++ b/packages/userscript/src/locales/en.json @@ -29,6 +29,8 @@ "Enable on HTML": "Enable on HTML files", "Enable on Markdown": "Enable on Markdown files", "Use 24-hour format": "Use 24-hour format (eg. 23:59)", + "Export Format": "Export Format", + "Export JSON Format Description": "Export JSON in OpenAI Official Format", "Export Metadata": "Export Metadata", "Export Metadata Description": "Add metadata to exported Markdown and HTML files.", "Conversation Delete Alert": "Are you sure you want to delete all selected conversations?", diff --git a/packages/userscript/src/locales/es.json b/packages/userscript/src/locales/es.json index cef2822..f936108 100644 --- a/packages/userscript/src/locales/es.json +++ b/packages/userscript/src/locales/es.json @@ -29,6 +29,8 @@ "Enable on HTML": "Habilitar en archivos HTML", "Enable on Markdown": "Habilitar en archivos Markdown", "Use 24-hour format": "Usar formato de 24 horas(ej. 23:59)", + "Export Format": "Formato de Exportación", + "Export JSON Format Description": "Exportar JSON en el Formato Oficial de OpenAI", "Export Metadata": "Exportar Metadata", "Export Metadata Description": "Añadir Metadata a Markdown y a HTML exportados.", "Conversation Delete Alert": "¿Estás seguro que quieres borrar todas las conversaciones seleccionadas?", diff --git a/packages/userscript/src/locales/id.json b/packages/userscript/src/locales/id.json index 017aee4..bff73e8 100644 --- a/packages/userscript/src/locales/id.json +++ b/packages/userscript/src/locales/id.json @@ -29,6 +29,8 @@ "Enable on HTML": "Aktifkan pada file HTML", "Enable on Markdown": "Aktifkan pada file Markdown", "Use 24-hour format": "Gunakan format 24 jam (mis. 23:59)", + "Export Format": "Format Ekspor", + "Export JSON Format Description": "Ekspor JSON dalam Format Resmi OpenAI", "Export Metadata": "Ekspor Metadata", "Export Metadata Description": "Tambahkan metadata ke file Markdown dan HTML yang diekspor.", "Conversation Delete Alert": "Apakah Anda yakin ingin menghapus semua percakapan yang dipilih?", diff --git a/packages/userscript/src/locales/jp.json b/packages/userscript/src/locales/jp.json index 70b93ac..1e28fe3 100644 --- a/packages/userscript/src/locales/jp.json +++ b/packages/userscript/src/locales/jp.json @@ -29,6 +29,8 @@ "Enable on HTML": "HTML ファイルで有効にする", "Enable on Markdown": "Markdown ファイルで有効にする", "Use 24-hour format": "24時間形式を使用する (例: 23:59)", + "Export Format": "エクスポートフォーマット", + "Export JSON Format Description": "OpenAI公式フォーマットでのJSONのエクスポート", "Export Metadata": "メタデータをエクスポート", "Export Metadata Description": "エクスポートされたMarkdownおよびHTMLファイルにメタデータを追加します。", "Conversation Delete Alert": "選択したすべての会話を削除してもよろしいですか?", diff --git a/packages/userscript/src/locales/tr.json b/packages/userscript/src/locales/tr.json index c5db484..2319cf5 100644 --- a/packages/userscript/src/locales/tr.json +++ b/packages/userscript/src/locales/tr.json @@ -29,6 +29,8 @@ "Enable on HTML": "HTML dosyalarında etkinleştir", "Enable on Markdown": "Markdown dosyalarında etkinleştir", "Use 24-hour format": "24 saat biçimini kullan (örn. 23:59)", + "Export Format": "Dışa Aktarma Formatı", + "Export JSON Format Description": "OpenAI Resmi Formatında JSON Dışa Aktarma", "Export Metadata": "Üst veriyi dışa aktar", "Export Metadata Description": "Dışa aktarılan Markdown ve HTML dosyalarına üst veri ekle", "Conversation Delete Alert": "Seçilen tüm konuşmaları silmek istediğinizden emin misiniz?", diff --git a/packages/userscript/src/locales/zh-Hans.json b/packages/userscript/src/locales/zh-Hans.json index c741534..7dddada 100644 --- a/packages/userscript/src/locales/zh-Hans.json +++ b/packages/userscript/src/locales/zh-Hans.json @@ -29,6 +29,8 @@ "Enable on HTML": "在 HTML 文件上启用", "Enable on Markdown": "在 Markdown 文件上启用", "Use 24-hour format": "使用24小时制 (例如 23:59)", + "Export Format": "导出格式", + "Export JSON Format Description": "以 OpenAI 官方格式导出JSON", "Export Metadata": "导出元数据", "Export Metadata Description": "会添加至 Markdown 以及 HTML 导出。", "Conversation Delete Alert": "确定要删除所有选取的对话?", diff --git a/packages/userscript/src/locales/zh-Hant.json b/packages/userscript/src/locales/zh-Hant.json index 9802734..fae6016 100644 --- a/packages/userscript/src/locales/zh-Hant.json +++ b/packages/userscript/src/locales/zh-Hant.json @@ -29,6 +29,8 @@ "Enable on HTML": "在 HTML 檔案上啟用", "Enable on Markdown": "在 Markdown 檔案上啟用", "Use 24-hour format": "使用24小時制 (例如 23:59)", + "Export Format": "匯出格式", + "Export JSON Format Description": "以 OpenAI 官方格式匯出 JSON", "Export Metadata": "匯出元資料", "Export Metadata Description": "會添加至 Markdown 以及 HTML 匯出。", "Conversation Delete Alert": "確定要刪除所有選取的對話?", diff --git a/packages/userscript/src/ui/ExportDialog.tsx b/packages/userscript/src/ui/ExportDialog.tsx index 3289ac3..c7d8a0e 100644 --- a/packages/userscript/src/ui/ExportDialog.tsx +++ b/packages/userscript/src/ui/ExportDialog.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks' import { useTranslation } from 'react-i18next' import { deleteConversation, fetchAllConversations, fetchConversation } from '../api' import { exportAllToHtml } from '../exporter/html' -import { exportAllToJson } from '../exporter/json' +import { exportAllToJson, exportAllToOfficialJson } from '../exporter/json' import { exportAllToMarkdown } from '../exporter/markdown' import { RequestQueue } from '../utils/queue' import { CheckBox } from './CheckBox' @@ -13,12 +13,6 @@ import type { ApiConversationItem, ApiConversationWithId } from '../api' import type { FC } from '../type' import type { ChangeEvent } from 'preact/compat' -const exportAllOptions = [ - { label: 'Markdown', callback: exportAllToMarkdown }, - { label: 'JSON', callback: exportAllToJson }, - { label: 'HTML', callback: exportAllToHtml }, -] - interface ConversationSelectProps { conversations: ApiConversationItem[] selected: ApiConversationItem[] @@ -83,9 +77,15 @@ type ExportSource = 'API' | 'Local' export const ExportDialog: FC = ({ format, open, onOpenChange, children }) => { const { t } = useTranslation() - const { enableMeta, exportMetaList } = useSettingContext() + const { enableMeta, exportMetaList, exportOfficialJsonFormat } = useSettingContext() const metaList = useMemo(() => enableMeta ? exportMetaList : [], [enableMeta, exportMetaList]) + const exportAllOptions = useMemo(() => [ + { label: 'Markdown', callback: exportAllToMarkdown }, + { label: 'JSON', callback: exportOfficialJsonFormat ? exportAllToOfficialJson : exportAllToJson }, + { label: 'HTML', callback: exportAllToHtml }, + ], [exportOfficialJsonFormat]) + const fileInputRef = useRef(null) const [exportSource, setExportSource] = useState('API') const [apiConversations, setApiConversations] = useState([]) @@ -152,7 +152,7 @@ export const ExportDialog: FC = ({ format, open, onOpenChange if (callback) callback(format, results, metaList) }) return () => off() - }, [requestQueue, exportType, format, metaList]) + }, [requestQueue, exportAllOptions, exportType, format, metaList]) useEffect(() => { const off = deleteQueue.on('done', () => { @@ -185,7 +185,7 @@ export const ExportDialog: FC = ({ format, open, onOpenChange const results = localConversations.filter(c => selected.some(s => s.id === c.id)) const callback = exportAllOptions.find(o => o.label === exportType)?.callback if (callback) callback(format, results, metaList) - }, [disabled, selected, localConversations, exportType, format, metaList]) + }, [disabled, selected, localConversations, exportAllOptions, exportType, format, metaList]) const exportAll = useMemo(() => { return exportSource === 'API' ? exportAllFromApi : exportAllFromLocal diff --git a/packages/userscript/src/ui/Menu.tsx b/packages/userscript/src/ui/Menu.tsx index 40872c7..30c8108 100644 --- a/packages/userscript/src/ui/Menu.tsx +++ b/packages/userscript/src/ui/Menu.tsx @@ -28,6 +28,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) { const { format, + exportOfficialJsonFormat, enableTimestamp, timeStamp24H, enableMeta, @@ -49,7 +50,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) { const onClickPng = useCallback(() => exportToPng(format), [format]) const onClickMarkdown = useCallback(() => exportToMarkdown(format, metaList), [format, metaList]) const onClickHtml = useCallback(() => exportToHtml(format, metaList), [format, metaList]) - const onClickJSON = useCallback(() => exportToJson(format), [format]) + const onClickJSON = useCallback(() => exportToJson(format, { officialFormat: exportOfficialJsonFormat }), [format, exportOfficialJsonFormat]) const width = useWindowResize(() => window.innerWidth) const isMobile = width < 768 diff --git a/packages/userscript/src/ui/SettingContext.tsx b/packages/userscript/src/ui/SettingContext.tsx index bc8c4ab..192085b 100644 --- a/packages/userscript/src/ui/SettingContext.tsx +++ b/packages/userscript/src/ui/SettingContext.tsx @@ -4,6 +4,7 @@ import { KEY_FILENAME_FORMAT, KEY_META_ENABLED, KEY_META_LIST, + KEY_OFFICIAL_JSON_FORMAT, KEY_TIMESTAMP_24H, KEY_TIMESTAMP_ENABLED, KEY_TIMESTAMP_HTML, @@ -28,6 +29,9 @@ const SettingContext = createContext({ format: defaultFormat, setFormat: (_: string) => {}, + exportOfficialJsonFormat: false, + setExportOfficialJsonFormat: (_: boolean) => {}, + enableTimestamp: false, setEnableTimestamp: (_: boolean) => {}, timeStamp24H: false, @@ -48,6 +52,8 @@ const SettingContext = createContext({ export const SettingProvider: FC = ({ children }) => { const [format, setFormat] = useGMStorage(KEY_FILENAME_FORMAT, defaultFormat) + const [exportOfficialJsonFormat, setExportOfficialJsonFormat] = useGMStorage(KEY_OFFICIAL_JSON_FORMAT, false) + const [enableTimestamp, setEnableTimestamp] = useGMStorage(KEY_TIMESTAMP_ENABLED, false) const [timeStamp24H, setTimeStamp24H] = useGMStorage(KEY_TIMESTAMP_24H, false) const [enableTimestampHTML, setEnableTimestampHTML] = useGMStorage(KEY_TIMESTAMP_HTML, false) @@ -69,6 +75,9 @@ export const SettingProvider: FC = ({ children }) => { format, setFormat, + exportOfficialJsonFormat, + setExportOfficialJsonFormat, + enableTimestamp, setEnableTimestamp, timeStamp24H, diff --git a/packages/userscript/src/ui/SettingDialog.tsx b/packages/userscript/src/ui/SettingDialog.tsx index 5f192ac..82f3fd0 100644 --- a/packages/userscript/src/ui/SettingDialog.tsx +++ b/packages/userscript/src/ui/SettingDialog.tsx @@ -32,6 +32,7 @@ export const SettingDialog: FC = ({ timeStamp24H, setTimeStamp24H, enableTimestampHTML, setEnableTimestampHTML, enableTimestampMarkdown, setEnableTimestampMarkdown, + exportOfficialJsonFormat, setExportOfficialJsonFormat, enableMeta, setEnableMeta, exportMetaList, setExportMetaList, } = useSettingContext() @@ -108,6 +109,22 @@ export const SettingDialog: FC = ({ +
+
+
+ {t('Export Format')} +
+
+
+ +
+
+
+