fix: set a hard cap 100 on export conversations to avoid OOM

This commit is contained in:
pionxzh
2026-03-01 23:07:13 +08:00
parent 9ce72c0f05
commit 2fd3516993
11 changed files with 132 additions and 32 deletions

View File

@@ -43,5 +43,8 @@
"(no project)": "(no project)",
"Export All Limit": "Export All Limit",
"Export All Limit Description": "Set the maximum number of conversations to load in the 'Export All' dialog.",
"Select a source to load conversations": "Select a project above to load conversations."
"Select a source to load conversations": "Select a project above to load conversations.",
"Search": "Search",
"Last 100": "Last 100",
"No results": "No results"
}

View File

@@ -43,5 +43,8 @@
"(no project)": "(sin proyecto)",
"Export All Limit": "Límite de Exportar Todos",
"Export All Limit Description": "Establece el número máximo de conversaciones a cargar en el diálogo 'Exportar Todos'.",
"Select a source to load conversations": "Selecciona un proyecto arriba para cargar conversaciones."
"Select a source to load conversations": "Selecciona un proyecto arriba para cargar conversaciones.",
"Search": "Buscar",
"Last 100": "Últimas 100",
"No results": "Sin resultados"
}

View File

@@ -43,5 +43,8 @@
"(no project)": "(aucun projet)",
"Export All Limit": "Limite d'Exportation Multiple",
"Export All Limit Description": "Définit le nombre maximal de conversations à charger dans la boîte de dialogue 'Tout exporter'.",
"Select a source to load conversations": "Sélectionnez un projet ci-dessus pour charger les conversations."
"Select a source to load conversations": "Sélectionnez un projet ci-dessus pour charger les conversations.",
"Search": "Rechercher",
"Last 100": "100 dernières",
"No results": "Aucun résultat"
}

View File

@@ -43,5 +43,8 @@
"(no project)": "(tidak ada proyek)",
"Export All Limit": "Batas Ekspor Semua",
"Export All Limit Description": "Atur jumlah maksimum percakapan yang akan dimuat dalam dialog 'Ekspor Semua'.",
"Select a source to load conversations": "Pilih proyek di atas untuk memuat percakapan."
"Select a source to load conversations": "Pilih proyek di atas untuk memuat percakapan.",
"Search": "Cari",
"Last 100": "100 Terakhir",
"No results": "Tidak ada hasil"
}

View File

@@ -43,5 +43,8 @@
"(no project)": "(プロジェクトなし)",
"Export All Limit": "すべてエクスポートの上限",
"Export All Limit Description": "「すべてエクスポート」ダイアログで読み込む会話の最大数を設定します。",
"Select a source to load conversations": "上からプロジェクトを選択して会話を読み込んでください。"
"Select a source to load conversations": "上からプロジェクトを選択して会話を読み込んでください。",
"Search": "検索",
"Last 100": "最新100件",
"No results": "結果なし"
}

View File

@@ -43,5 +43,8 @@
"(no project)": "(нет проекта)",
"Export All Limit": "Лимит экспорта всех",
"Export All Limit Description": "Установите максимальное количество бесед для загрузки в диалоге 'Экспортировать все'.",
"Select a source to load conversations": "Выберите проект выше, чтобы загрузить беседы."
"Select a source to load conversations": "Выберите проект выше, чтобы загрузить беседы.",
"Search": "Поиск",
"Last 100": "Последние 100",
"No results": "Нет результатов"
}

View File

@@ -43,5 +43,8 @@
"(no project)": "(proje yok)",
"Export All Limit": "Tümünü Dışa Aktarma Limiti",
"Export All Limit Description": "'Tümünü Dışa Aktar' iletişim kutusunda yüklenecek maksimum konuşma sayısını ayarlayın.",
"Select a source to load conversations": "Konuşmaları yüklemek için yukarıdan bir proje seçin."
"Select a source to load conversations": "Konuşmaları yüklemek için yukarıdan bir proje seçin.",
"Search": "Ara",
"Last 100": "Son 100",
"No results": "Sonuç yok"
}

View File

@@ -43,5 +43,8 @@
"(no project)": "(无项目)",
"Export All Limit": "批量导出上限",
"Export All Limit Description": "设置“批量导出”对话框中加载的最大对话数量。",
"Select a source to load conversations": "请在上方选择一个项目以加载对话。"
"Select a source to load conversations": "请在上方选择一个项目以加载对话。",
"Search": "搜索",
"Last 100": "最新 100 条",
"No results": "无结果"
}

View File

@@ -43,5 +43,8 @@
"(no project)": "(無專案)",
"Export All Limit": "批量匯出上限",
"Export All Limit Description": "設定「批量匯出」對話方塊中載入的最大對話數量。",
"Select a source to load conversations": "請在上方選擇一個專案以載入對話。"
"Select a source to load conversations": "請在上方選擇一個專案以載入對話。",
"Search": "搜尋",
"Last 100": "最新 100 條",
"No results": "無結果"
}

View File

@@ -66,9 +66,27 @@
background-color: #f9d9d9;
color: #a71d2a;
}
.Button.neutral {
background-color: transparent;
color: #6f6e77;
border: 1px solid #6f6e77;
font-size: 13px;
height: 26px;
padding: 0 8px;
}
.Button.green:hover {
background-color: #ccebd7;
}
.Button.neutral:hover {
background-color: rgba(111, 110, 119, 0.1);
}
.dark .Button.neutral {
color: #a0a0a8;
border-color: #a0a0a8;
}
.dark .Button.neutral:hover {
background-color: rgba(160, 160, 168, 0.1);
}
.Button:disabled {
opacity: 0.5;
color: #6f6e77;
@@ -151,11 +169,26 @@
color: #bcbcbc;
}
.SelectSearch {
width: 100%;
padding: 8px 16px;
border: 1px solid #6f6e77;
border-bottom: none;
border-radius: 4px 4px 0 0;
background-color: transparent;
color: inherit;
font-size: 14px;
outline: none;
}
.SelectSearch::placeholder {
color: #9ca3af;
}
.SelectToolbar {
display: flex;
align-items: center;
padding: 12px 16px;
border-radius: 4px 4px 0 0;
border-radius: 0;
border: 1px solid #6f6e77;
border-bottom: none;
}

View File

@@ -50,6 +50,8 @@ const ProjectSelect: FC<ProjectSelectProps> = ({ projects, selected, setSelected
)
}
const EXPORT_LIMIT = 100
interface ConversationSelectProps {
conversations: ApiConversationItem[]
selected: ApiConversationItem[]
@@ -68,43 +70,80 @@ const ConversationSelect: FC<ConversationSelectProps> = ({
error,
}) => {
const { t } = useTranslation()
const [query, setQuery] = useState('')
const filtered = useMemo(() => {
const q = query.trim().toLowerCase()
if (!q) return conversations
return conversations.filter(c => c.title.toLowerCase().includes(q))
}, [conversations, query])
const atCap = selected.length >= EXPORT_LIMIT
const allFilteredSelected = filtered.length > 0
&& filtered.slice(0, EXPORT_LIMIT).every(c => selected.some(x => x.id === c.id))
return (
<>
<input
type="search"
className="SelectSearch"
placeholder={t('Search')}
value={query}
onInput={e => setQuery(e.currentTarget.value)}
/>
<div className="SelectToolbar">
<CheckBox
label={t('Select All')}
disabled={disabled}
checked={selected.length === conversations.length}
checked={allFilteredSelected}
onCheckedChange={(checked) => {
setSelected(checked ? conversations : [])
setSelected(checked ? filtered.slice(0, EXPORT_LIMIT) : [])
}}
/>
{loading && conversations.length > 0 && (
<span className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<IconLoading className="w-3 h-3" />
{t('Loading')}... ({conversations.length})
<div className="flex items-center gap-2 ml-auto">
{loading && conversations.length > 0 && (
<span className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<IconLoading className="w-3 h-3" />
{t('Loading')}... ({conversations.length})
</span>
)}
<button
className="Button neutral"
disabled={disabled || conversations.length === 0}
onClick={() => setSelected(conversations.slice(0, EXPORT_LIMIT))}
>
{t('Last 100')}
</button>
<span className={`text-sm font-medium tabular-nums ${atCap ? 'text-red-500 dark:text-red-400' : 'text-gray-500 dark:text-gray-400'}`}>
{selected.length}/{EXPORT_LIMIT}
</span>
)}
</div>
</div>
<ul className="SelectList">
{loading && conversations.length === 0 && <li className="SelectItem">{t('Loading')}...</li>}
{error && <li className="SelectItem">{t('Error')}: {error}</li>}
{conversations.map(c => (
<li className="SelectItem" key={c.id}>
<CheckBox
label={c.title}
disabled={disabled}
checked={selected.some(x => x.id === c.id)}
onCheckedChange={(checked) => {
setSelected(checked
? [...selected, c]
: selected.filter(x => x.id !== c.id),
)
}}
/>
</li>
))}
{filtered.map((c) => {
const isSelected = selected.some(x => x.id === c.id)
return (
<li className="SelectItem" key={c.id}>
<CheckBox
label={c.title}
disabled={disabled || (atCap && !isSelected)}
checked={isSelected}
onCheckedChange={(checked) => {
if (checked && atCap) return
setSelected(checked
? [...selected, c]
: selected.filter(x => x.id !== c.id),
)
}}
/>
</li>
)
})}
{!loading && !error && filtered.length === 0 && conversations.length > 0 && (
<li className="SelectItem text-gray-400 dark:text-gray-500">{t('No results')}</li>
)}
</ul>
</>
)
@@ -353,6 +392,7 @@ const DialogContent: FC<DialogContentProps> = ({ format }) => {
)
: (
<ConversationSelect
key={selectedProject?.id ?? 'no-project'}
conversations={conversations}
selected={selected}
setSelected={setSelected}