mirror of
https://github.com/pionxzh/chatgpt-exporter.git
synced 2026-09-21 13:37:42 -05:00
feat(export): select not-exported or updated conversations
This commit is contained in:
@@ -17,6 +17,7 @@ export const KEY_META_LIST = 'exporter:meta_list'
|
||||
export const KEY_THINKING_ENABLED = 'exporter:enable_thinking'
|
||||
export const KEY_SOURCES_ENABLED = 'exporter:enable_sources'
|
||||
export const KEY_EXPORT_ALL_LIMIT = 'exporter:export_all_limit'
|
||||
export const KEY_EXPORTED_UPDATE_TIMES = 'exporter:exported_update_times'
|
||||
|
||||
export const KEY_OAI_LOCALE = 'oai/apps/locale'
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"Save": "Save",
|
||||
"Delete": "Delete",
|
||||
"Select All": "Select All",
|
||||
"Select...": "Select...",
|
||||
"Select Not Exported": "Select Not Exported",
|
||||
"Select Updated": "Select Updated",
|
||||
"Export": "Export",
|
||||
"Error": "Error",
|
||||
"Loading": "Loading",
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"Save": "Guardar",
|
||||
"Delete": "Borrar",
|
||||
"Select All": "Seleccionar Todos",
|
||||
"Select...": "Seleccionar...",
|
||||
"Select Not Exported": "Seleccionar no exportadas",
|
||||
"Select Updated": "Seleccionar actualizadas",
|
||||
"Export": "Exportar",
|
||||
"Error": "Error",
|
||||
"Loading": "Cargando",
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"Save": "Enregistrer",
|
||||
"Delete": "Supprimer",
|
||||
"Select All": "Tout sélectionner",
|
||||
"Select...": "Sélectionner...",
|
||||
"Select Not Exported": "Sélectionner non exportées",
|
||||
"Select Updated": "Sélectionner mises à jour",
|
||||
"Export": "Exporter",
|
||||
"Error": "Erreur",
|
||||
"Loading": "Chargement",
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"Save": "Simpan",
|
||||
"Delete": "Hapus",
|
||||
"Select All": "Pilih Semua",
|
||||
"Select...": "Pilih...",
|
||||
"Select Not Exported": "Pilih yang belum diekspor",
|
||||
"Select Updated": "Pilih yang diperbarui",
|
||||
"Export": "Ekspor",
|
||||
"Error": "Kesalahan",
|
||||
"Loading": "Memuat",
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"Save": "保存",
|
||||
"Delete": "削除",
|
||||
"Select All": "すべて選択",
|
||||
"Select...": "選択...",
|
||||
"Select Not Exported": "未エクスポートを選択",
|
||||
"Select Updated": "更新されたものを選択",
|
||||
"Export": "エクスポート",
|
||||
"Error": "エラー",
|
||||
"Loading": "読み込み中",
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"Save": "Сохранить",
|
||||
"Delete": "Удалить",
|
||||
"Select All": "Выбрать все",
|
||||
"Select...": "Выбрать...",
|
||||
"Select Not Exported": "Выбрать неэкспортированные",
|
||||
"Select Updated": "Выбрать обновлённые",
|
||||
"Export": "Экспорт",
|
||||
"Error": "Ошибка",
|
||||
"Loading": "Загрузка",
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"Save": "Kaydet",
|
||||
"Delete": "Sil",
|
||||
"Select All": "Tümünü Seç",
|
||||
"Select...": "Seç...",
|
||||
"Select Not Exported": "Dışa aktarılmamışları seç",
|
||||
"Select Updated": "Güncellenenleri seç",
|
||||
"Export": "Dışa Aktar",
|
||||
"Error": "Hata",
|
||||
"Loading": "Yükleniyor",
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"Save": "保存",
|
||||
"Delete": "删除",
|
||||
"Select All": "全选",
|
||||
"Select...": "选择…",
|
||||
"Select Not Exported": "选择未导出",
|
||||
"Select Updated": "选择有更新",
|
||||
"Export": "导出",
|
||||
"Error": "错误",
|
||||
"Loading": "加载中",
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"Save": "保存",
|
||||
"Delete": "刪除",
|
||||
"Select All": "全選",
|
||||
"Select...": "選取…",
|
||||
"Select Not Exported": "選取未匯出",
|
||||
"Select Updated": "選取有更新",
|
||||
"Export": "匯出",
|
||||
"Error": "錯誤",
|
||||
"Loading": "載入中",
|
||||
|
||||
+53
-1
@@ -2,11 +2,12 @@ import * as Dialog from '@radix-ui/react-dialog'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { archiveConversation, deleteConversation, fetchAllConversations, fetchConversation, fetchConversationsPage, fetchProjects, probeApi } from '../api'
|
||||
import { EXPORT_OPERATION_BATCH } from '../constants'
|
||||
import { EXPORT_OPERATION_BATCH, KEY_EXPORTED_UPDATE_TIMES } from '../constants'
|
||||
import { exportAllToHtml } from '../exporter/html'
|
||||
import { exportAllToJson, exportAllToOfficialJson } from '../exporter/json'
|
||||
import { exportAllToMarkdown } from '../exporter/markdown'
|
||||
import { RequestQueue } from '../utils/queue'
|
||||
import { ScriptStorage } from '../utils/storage'
|
||||
import { sleep } from '../utils/utils'
|
||||
import { CheckBox } from './CheckBox'
|
||||
import { IconCross, IconLoading, IconUpload } from './Icons'
|
||||
@@ -56,6 +57,24 @@ function formatConvDate(time: number | string | undefined): string {
|
||||
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
/** Read the persisted per-conversation last-exported update_time map (conversation id → ms). */
|
||||
function getExportedUpdateTimes(): Record<string, number> {
|
||||
const stored = ScriptStorage.get<Record<string, number>>(KEY_EXPORTED_UPDATE_TIMES)
|
||||
if (stored && typeof stored === 'object') return stored
|
||||
return {}
|
||||
}
|
||||
|
||||
/** Persist the last-exported update_time for conversations that were actually exported successfully. */
|
||||
function markExported(conversations: { id: string; update_time?: number | string }[]): void {
|
||||
if (conversations.length === 0) return
|
||||
const map = getExportedUpdateTimes()
|
||||
for (const c of conversations) {
|
||||
const ms = toMs(c.update_time)
|
||||
if (ms > (map[c.id] ?? 0)) map[c.id] = ms
|
||||
}
|
||||
ScriptStorage.set(KEY_EXPORTED_UPDATE_TIMES, map)
|
||||
}
|
||||
|
||||
/** Text search supporting * and ? wildcards. Falls back to substring. */
|
||||
function textSearch(title: string, query: string): boolean {
|
||||
const q = query.trim()
|
||||
@@ -162,6 +181,20 @@ const ConversationSelect: FC<ConversationSelectProps> = ({
|
||||
|
||||
const allFilteredSelected = filtered.length > 0 && filtered.every(c => selected.some(x => x.id === c.id))
|
||||
|
||||
const selectByExportStatus = useCallback((status: 'all' | 'not_exported' | 'updated') => {
|
||||
lastClickedIndex.current = -1
|
||||
const exportedMap = getExportedUpdateTimes()
|
||||
if (status === 'all') {
|
||||
setSelected(filtered)
|
||||
}
|
||||
else if (status === 'not_exported') {
|
||||
setSelected(filtered.filter(c => !(c.id in exportedMap)))
|
||||
}
|
||||
else {
|
||||
setSelected(filtered.filter(c => c.id in exportedMap && exportedMap[c.id] < toMs(c.update_time)))
|
||||
}
|
||||
}, [filtered, setSelected])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── Search input ── */}
|
||||
@@ -203,6 +236,22 @@ const ConversationSelect: FC<ConversationSelectProps> = ({
|
||||
>
|
||||
{t('Last 100')}
|
||||
</button>
|
||||
<select
|
||||
className="Select"
|
||||
style={{ fontSize: '0.75rem', padding: '2px 5px' }}
|
||||
disabled={disabled || filtered.length === 0}
|
||||
value=""
|
||||
title="Select conversations by export status"
|
||||
onChange={(e) => {
|
||||
const val = e.currentTarget.value
|
||||
if (val) selectByExportStatus(val as 'all' | 'not_exported' | 'updated')
|
||||
}}
|
||||
>
|
||||
<option value="" disabled>{t('Select...')}</option>
|
||||
<option value="all">{t('Select All')}</option>
|
||||
<option value="not_exported">{t('Select Not Exported')}</option>
|
||||
<option value="updated">{t('Select Updated')}</option>
|
||||
</select>
|
||||
{/* Resume control: select the next 100 starting at a given offset */}
|
||||
<input
|
||||
type="number"
|
||||
@@ -480,6 +529,8 @@ const DialogContent: FC<DialogContentProps> = ({ format }) => {
|
||||
const callback = exportAllOptions.find(o => o.label === exportType)?.callback
|
||||
if (callback && results.length > 0) {
|
||||
await callback(format, results, metaList, selectedProject?.display.name, partIndex, totalBatches)
|
||||
// Only conversations that were actually exported successfully get recorded
|
||||
markExported(results)
|
||||
}
|
||||
if (partIndex < totalBatches) {
|
||||
await sleep(400)
|
||||
@@ -550,6 +601,7 @@ const DialogContent: FC<DialogContentProps> = ({ format }) => {
|
||||
setProcessing(true)
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
await callback(format, chunks[i], metaList, selectedProject?.display.name, i + 1, chunks.length)
|
||||
markExported(chunks[i])
|
||||
if (i < chunks.length - 1) await sleep(400)
|
||||
}
|
||||
setProcessing(false)
|
||||
|
||||
Reference in New Issue
Block a user