* feat(export): batch export in 100-conv waves and date-range filter
Closes#347 (feature request on upstream pionxzh/chatgpt-exporter).
Changes:
- src/constants.ts: add EXPORT_OPERATION_BATCH = 100 (fixed, non-tunable)
- src/utils/download.ts: add PartInfo type, part-suffix support to
buildZipFileName, and new buildJsonBatchFileName for official JSON
- src/exporter/{html,json,markdown}.ts: accept optional partIndex /
totalParts params; each exportAllTo* uses PartInfo to suffix its
output filename when multiple batches are produced
- src/ui/ExportDialog.tsx:
- Remove hardcoded EXPORT_LIMIT = 100 selection cap; users may now
select all loaded conversations (up to exportAllLimit from settings)
- Add DateFilter component with from/to date inputs that filter the
conversation list by create_time (inclusive, end-of-day for dateTo)
- Wave-based export pipeline: splits selected conversations into
chunks of EXPORT_OPERATION_BATCH (100), fetches each chunk via
RequestQueue, downloads the completed batch, pauses 400 ms, then
continues with the next chunk — keeping memory bounded and API
pressure low
- Progress bar now shows global count across all batches plus
"Batch X/N" indicator when multiple batches are active
- Local-file source also batched in 100-conversation waves with the
same 400 ms inter-batch delay
- Shift-click range selection and Select All no longer impose a
hard cap (handled by the batch pipeline instead)
- src/locales/en.json: add Date Filter Label/Hint, Selected count,
Export batch info, Exporting batch strings; update Export All Limit
Description to mention 100-conversation waves
Made-with: Cursor
* fix(export): correct date filter for ISO string timestamps + add update_time field
The list endpoint returns create_time/update_time as ISO 8601 strings
(e.g. "2025-07-10T14:53:58.103234Z"), not Unix numbers. The previous
filter compared a string to a number, always producing NaN comparisons
and filtering out every conversation.
- Add toMs() helper that handles both ISO strings and legacy Unix numbers
- Fix filter comparisons to use toMs() and compare in milliseconds
- Update ApiConversationItem type: create_time is number|string,
update_time is optional number|string
- Add filterField state ('create_time' | 'update_time') with a
Created/Updated selector in the DateFilter UI so users can filter
by either timestamp
- Add i18n strings for the new selector labels
Made-with: Cursor
* feat: redesign date filter layout with presets, auto-load conversations, batch label on export
- Date filter redesigned to two-row layout: field selector + quick preset
buttons (7d / 30d / 90d / This year / Clear) on row 1; From–To date
inputs on row 2, eliminating the wrapping issue
- Auto-load all conversations when dialog opens (default project null
instead of undefined) so search box, Last 100 button, and counter are
immediately visible without needing to manually pick a project
- Export button shows batch count when selection > 100, e.g. "Export · 3 files"
with a tooltip explaining "3 separate downloads, 100 conversations each"
- Progress display shows "Batch X/Y · completed/total" for multi-batch exports
- Remove redundant "Export from API" static text (project selector implies it)
- Add locale strings: Date Preset 7d/30d/90d/Year, Clear filter, Batch progress
Made-with: Cursor
* fix: show batch download count inline below export button
Made-with: Cursor
* feat: filter chips, date column, All conversations, starred indicator
- Filter chips via # trigger: type # in search box to open a popover
with available filters; click to add as a removable chip:
★ Starred only | 💾 Saved only (skip temp) | ⏱ Long conversation
(active ≥ N days, editable inline) | 🤖 GPT/custom AI | 💬 Regular chats
- Date column: each conversation row now shows a compact relative date
(Today / Yesterday / Xd ago / Mon DD / MMM YYYY) on the right edge
- Starred indicator: ★ shown inline for starred conversations
- Project dropdown: new "📂 All conversations" option fetches the main
list plus every project's conversations, deduplicated; uses new
fetchAllConversationsAll() API function
- ApiConversationItem: add is_starred, is_temporary_chat, gizmo_id fields
from the list endpoint payload
- CSS: SelectItem flex layout, new SelectChip / SelectChips /
SelectFilterPopover / SelectItemMeta classes
Made-with: Cursor
* feat: add Load More button to fetch conversations beyond the initial limit
- Export fetchConversationsPage from api.ts for single-page incremental loading
- Add onHasMore callback to fetchAllConversations so callers know when the
fetch was stopped by the user-configured limit (vs the API having no more data)
- DialogContent tracks hasMore / totalAvailable / loadingMore state
- A "Load N more" button appears below the conversation list when more pages
exist; shows "N remaining" count once the API total is known
- Resets cleanly on project change
Made-with: Cursor
* feat: comprehensive filter chip system with chat class, origin, status, and mode logic
- Add conversation_origin, pinned_time, is_archived, is_do_not_remember to ApiConversationItem
- Replace simple chip flags with typed FilterChipDef union:
chat_class (Regular/GPT/Project), origin (dynamic from data),
status (starred/temporary/pinned), duration_gte, recency_lte
- Each chip has an include/exclude mode toggle (click badge to flip)
- AND/OR logic toggle appears in chip bar when 2+ chips are active
- Text search supports * and ? wildcards alongside chips
- Picker grouped by category with dynamic Origin values from loaded conversations
- Project classification cross-references fetched project IDs against gizmo_id
- Remove ProjectSelect dropdown: always auto-load all conversations on open
- Chat class chip: Regular (no gizmo), GPT (store gizmo), Project (matched gizmo)
Made-with: Cursor
* refactor(filters): redesign chips as multi-select with two-stage picker
- Chat class, Origin, Project, Status are now single chips that open a
sub-view where users tick multiple values (Regular + GPT, etc.)
- New Project chip lets users pick specific project(s) by name
- Date filter moved from top-of-dialog into the chip bar as a full-width
date-range chip (Created/Updated selector + date inputs + presets)
- Picker uses two-stage UX: root list → sub-view with checkboxes + Apply
- Clicking a chip's value label reopens the picker for in-place editing
- AND/OR logic toggle only appears for 2+ non-date chips
- Remove standalone DateFilter component and related state from DialogContent
Made-with: Cursor
* fix(filters): guard gizmo_id null check for project chip
Made-with: Cursor
* fix: prevent export dialog close mid-run and add resume-from-offset control
- Block ESC, outside-click, and X button while an export is in progress;
module-level exportingRef bridges DialogContent to ExportDialog without
lifting state.
- Stale-fetch prevention: fetchGenRef generation counter discards callbacks
from a previous dialog mount after remount, eliminating the console
errors seen when reopening the dialog mid-run.
- Cleanup on unmount: clear all three RequestQueues when DialogContent
unmounts so orphaned background work stops immediately.
- Add "→ 100 from #N" resume control in the toolbar: a number input sets
the starting offset and the arrow button selects the next 100 convs
from that position, letting users continue a partial export (e.g. set
offset to 200 to resume after 2 finished batches).
Made-with: Cursor
* perf: use Web Worker for sleep() to bypass background-tab timer throttle
Chrome throttles setTimeout in non-focused tabs to >=1 s minimum, which
causes the export queue (200 ms between requests * 100 convs = 20 s/batch)
to stall entirely when the user switches away from the tab.
Fix: run all timers inside a single persistent Web Worker. Workers are
exempt from background-tab throttling, so exports run at full speed
regardless of tab focus. A graceful fallback to regular setTimeout is
retained for environments where blob-URL Worker creation fails (CSP etc.)
so the change is entirely non-breaking.
Made-with: Cursor
* fix: proper 429 backoff — wait Retry-After secs, cap retries, skip when exhausted
Previously the queue retried forever with a max backoff of 1600 ms, causing
the 429 storm seen in the console (the same request hammering the API
continuously with no meaningful pause).
Changes:
- api.ts: export RateLimitError with retryAfterMs read from Retry-After
header (defaults to 30 s when header is absent or unparseable)
- queue.ts: on RateLimitError, sleep max(retryAfterMs, 30 s) before
retrying; give up after 8 consecutive 429s and skip the request; on
regular errors give up after 5 retries and skip; add rate_limited status
so the UI can show what is happening
- ExportDialog.tsx: show "Rate limited — waiting Xs…" with an amber
progress bar when status is rate_limited; clear progress bar to blue
once the pause ends
Made-with: Cursor
* fix: global queue pause on 429 instead of per-item backoff
The previous design retried each item individually with a 30s wait, so if
all 100 conversations in a batch were rate-limited the theoretical worst
case was 100 x 8 x 30s = 24,000s.
New design:
- On the first 429 the entire queue is frozen (pauseUntil timestamp).
Every subsequent process() call checks pauseUntil and waits out the
remaining time before making any new request — one shared wait for all
queued items, not N separate waits.
- The pause length is max(Retry-After header, 60s * pauseCount) so it
backs off exponentially if the first pause was not long enough:
pause 1 = 60s, pause 2 = 120s, pause 3 = 180s, etc.
- After MAX_GLOBAL_PAUSES (5) the queue aborts rather than looping forever
(total max wait ~15 min before giving up).
- The failed item is always returned to the front of the queue to be
retried after the pause clears rather than being counted as a skip.
- Removed per-item rateRetries counter (no longer needed).
Made-with: Cursor
* feat: add Test API button and rate-limit header logging
- probeApi(): make one minimal request (1 conversation) to check whether
the API is currently accepting traffic. Returns ok/rate_limited status
plus any X-RateLimit-* or Retry-After headers in the response.
- fetchApi(): logs all known rate-limit response headers to console on
every call (success or failure) so we can discover which headers
ChatGPT actually sends.
- ExportDialog: "Test API" button next to the upload icon. Shows
"Testing... / API ready / Rate limited · wait Xs / Error" inline.
Hovering the button after a successful test shows any rate-limit headers
as a tooltip (useful for debugging).
Made-with: Cursor
* fix: add Cancel button during export; replace useless dimmed X
Previously the X button was grayed out and non-functional while an export
was running, with no way to stop it short of refreshing the page.
Changes:
- Cancel button (red, top-right) appears while processing. Clicking it
sets cancelledRef, calls queue.stop() on all three queues, and lets the
in-flight request finish before cleanly stopping the loop.
- The on('done') handler checks cancelledRef: if set, it skips the
download and the next-batch kickoff and just resets processing state.
- cancelledRef is cleared at the start of each new export so back-to-back
exports work correctly.
- The dimmed X is kept (slightly more transparent) as a visual reminder
that the dialog cannot be closed mid-export; its tooltip now says
"Click Cancel to stop the export".
Made-with: Cursor
* feat: sortable column headers, both date columns, unambiguous date format
Problems fixed:
- Date column only showed create_time with no label — confusing when the
user had filtered or sorted by update_time.
- Dates < 1 year omitted the year ("Jun 17" vs "Jun 17, 2025"), making it
impossible to tell which year a conversation belonged to.
- No way to sort the list — order was whatever the API returned.
Changes:
- formatConvDate: always includes the year (removes the < 365-day branch).
Still shows "Today" / "Yesterday" for very recent items.
- ConversationSelect: two date columns (Created, Updated). The active-sort
column is visually highlighted (bold, blue) so the user always knows
what order the list is in.
- Sortable header row (SelectListHeader): click Title / Created / Updated
to sort; click again to toggle asc/desc. Arrow indicator (↑/↓/↕) shows
current state. Defaults to Created ↓ (newest first) matching the
original API order.
- Sorting applied in the filtered memo after chip/text filtering.
Made-with: Cursor
---------
Co-authored-by: Pionxzh <spigbbbbb@gmail.com>
When exporting conversations from a project, the zip filename becomes
chatgpt-export-{format}-project-{normalized-name}.zip instead of the
generic chatgpt-export-{format}.zip.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
With 400+ conversations the old code made 4+ sequential requests and
only rendered anything after all of them completed, freezing the dialog
for 10+ seconds with no interactable elements.
- Add optional `onBatch` callback to `fetchAllConversations` that fires
after each page arrives, so callers can react incrementally
- Wire `onBatch` in ExportDialog to append each batch to state as it
lands — first 100 conversations appear almost instantly
- Remove `loading` from the `disabled` guard so the user can select and
export already-loaded conversations while remaining pages still fetch
- Show "Loading…" only when the list is empty (initial blank state);
replace with a subtle italic sentinel at the bottom of the list while
subsequent pages are still in flight
- Allow the project selector to be changed at any time during loading
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Project conversations API switched from numeric offsets to opaque
alphanumeric cursors. The old code discarded the cursor and fell back
to offset += limit, so only the first page was ever exported.
- Add `cursor?: string | null` to `ApiConversations` interface
- Update `projectConversationsApi` to accept `string | number` cursor
- Expose the API-returned cursor in `fetchProjectConversations`
- Use cursor-based advancement in `fetchAllConversations` for project
requests, with numeric offset fallback for regular conversations
- Add early-exit guard when API returns neither total nor next cursor
Closes#341
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add support for alt_text content reference type (entity mentions)
- Fix Unicode normalization to include non-breaking hyphens and preserve newlines
- Render citations as inline links instead of footnotes
- Include all sources for multi-citations
- Fix HTML exporter to retain links instead of stripping them
Add support for getting project list. OpenAI
calls these 'gizmos' and they include custom GPTs
and other apps.
Only projects are supported for now.
Add project selection box in Export All dialog.