fix: stream conversation pages progressively to unblock Export All UI

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>
This commit is contained in:
pionxzh
2026-03-01 22:05:27 +08:00
parent b31d5cd565
commit e927de3658
2 changed files with 15 additions and 8 deletions

View File

@@ -548,7 +548,7 @@ async function fetchProjectConversations(project: string, cursor: string | numbe
}
}
export async function fetchAllConversations(project: string | null = null, maxConversations = 1000): Promise<ApiConversationItem[]> {
export async function fetchAllConversations(project: string | null = null, maxConversations = 1000, onBatch?: (batch: ApiConversationItem[]) => void): Promise<ApiConversationItem[]> {
const conversations: ApiConversationItem[] = []
const limit = project === null ? 100 : 50 // gizmos api uses a smaller limit
let offset = 0
@@ -565,6 +565,7 @@ export async function fetchAllConversations(project: string | null = null, maxCo
}
conversations.push(...result.items)
if (result.items.length === 0) break
onBatch?.(result.items)
// Stop if the API signals no more pages (no total count and no next cursor)
if (result.total == null && result.cursor == null) break
// Stop if we've reached the total reported by the API OR the user-defined limit

View File

@@ -77,10 +77,9 @@ const ConversationSelect: FC<ConversationSelectProps> = ({
/>
</div>
<ul className="SelectList">
{loading && <li className="SelectItem">{t('Loading')}...</li>}
{loading && conversations.length === 0 && <li className="SelectItem">{t('Loading')}...</li>}
{error && <li className="SelectItem">{t('Error')}: {error}</li>}
{!loading && !error
&& conversations.map(c => (
{conversations.map(c => (
<li className="SelectItem" key={c.id}>
<CheckBox
label={c.title}
@@ -95,6 +94,9 @@ const ConversationSelect: FC<ConversationSelectProps> = ({
/>
</li>
))}
{loading && conversations.length > 0 && (
<li className="SelectItem" style={{ opacity: 0.5, fontStyle: 'italic' }}>{t('Loading')}...</li>
)}
</ul>
</>
)
@@ -131,7 +133,7 @@ const DialogContent: FC<DialogContentProps> = ({ format }) => {
const [selected, setSelected] = useState<ApiConversationItem[]>([])
const [exportType, setExportType] = useState(exportAllOptions[0].label)
const disabled = loading || processing || !!error || selected.length === 0
const disabled = processing || !!error || selected.length === 0
const requestQueue = useMemo(() => new RequestQueue<ApiConversationWithId>(200, 1600), [])
const archiveQueue = useMemo(() => new RequestQueue<boolean>(200, 1600), [])
@@ -295,9 +297,13 @@ const DialogContent: FC<DialogContentProps> = ({ format }) => {
}, [])
useEffect(() => {
setApiConversations([])
setLoading(true)
fetchAllConversations(selectedProject?.id, exportAllLimit)
.then(setApiConversations)
fetchAllConversations(
selectedProject?.id,
exportAllLimit,
batch => setApiConversations(prev => [...prev, ...batch]),
)
.catch((err) => {
console.error('Error fetching conversations:', err)
setError(err.message || 'Failed to load conversations')
@@ -328,7 +334,7 @@ const DialogContent: FC<DialogContentProps> = ({ format }) => {
{t('Export from API')}
</div>
)}
<ProjectSelect projects={projects} selected={selectedProject} setSelected={setSelectedProject} disabled={processing || loading} />
<ProjectSelect projects={projects} selected={selectedProject} setSelected={setSelectedProject} disabled={processing} />
<ConversationSelect
conversations={conversations}
selected={selected}