feat: support OpenAI official JSON format

closes #162
This commit is contained in:
Pionxzh
2023-08-20 15:43:57 +08:00
parent 7558c36620
commit 66d8f90cb2
13 changed files with 66 additions and 14 deletions

View File

@@ -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'

View File

@@ -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<string, number>()
@@ -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)
}

View File

@@ -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?",

View File

@@ -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?",

View File

@@ -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?",

View File

@@ -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": "選択したすべての会話を削除してもよろしいですか?",

View File

@@ -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?",

View File

@@ -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": "确定要删除所有选取的对话?",

View File

@@ -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": "確定要刪除所有選取的對話?",

View File

@@ -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<ExportDialogProps> = ({ 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<HTMLInputElement>(null)
const [exportSource, setExportSource] = useState<ExportSource>('API')
const [apiConversations, setApiConversations] = useState<ApiConversationItem[]>([])
@@ -152,7 +152,7 @@ export const ExportDialog: FC<ExportDialogProps> = ({ 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<ExportDialogProps> = ({ 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

View File

@@ -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

View File

@@ -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,

View File

@@ -32,6 +32,7 @@ export const SettingDialog: FC<SettingDialogProps> = ({
timeStamp24H, setTimeStamp24H,
enableTimestampHTML, setEnableTimestampHTML,
enableTimestampMarkdown, setEnableTimestampMarkdown,
exportOfficialJsonFormat, setExportOfficialJsonFormat,
enableMeta, setEnableMeta,
exportMetaList, setExportMetaList,
} = useSettingContext()
@@ -108,6 +109,22 @@ export const SettingDialog: FC<SettingDialogProps> = ({
</dd>
</div>
</div>
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
<div>
<dt className="text-md font-medium text-gray-800 dark:text-white">
{t('Export Format')}
</dt>
<dd className="text-sm text-gray-700 dark:text-gray-300">
<div className="mt-2">
<Toggle
label={t('Export JSON Format Description')}
checked={exportOfficialJsonFormat}
onCheckedUpdate={setExportOfficialJsonFormat}
/>
</div>
</dd>
</div>
</div>
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
<div>
<dt className="text-md font-medium text-gray-800 dark:text-white">