gpt2api / web /src /app /settings /components /import-browser-dialog.tsx
jiayi.xie
Deploy chatgpt2api to HF Space (port 7860)
99a7ebb
Raw
History Blame Contribute Delete
8.3 kB
"use client";
import { Import, LoaderCircle, Search } from "lucide-react";
import { useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { PAGE_SIZE_OPTIONS, useSettingsStore } from "../store";
export function ImportBrowserDialog() {
const browserOpen = useSettingsStore((state) => state.browserOpen);
const browserPool = useSettingsStore((state) => state.browserPool);
const remoteFiles = useSettingsStore((state) => state.remoteFiles);
const selectedNames = useSettingsStore((state) => state.selectedNames);
const fileQuery = useSettingsStore((state) => state.fileQuery);
const filePage = useSettingsStore((state) => state.filePage);
const pageSize = useSettingsStore((state) => state.pageSize);
const isStartingImport = useSettingsStore((state) => state.isStartingImport);
const setBrowserOpen = useSettingsStore((state) => state.setBrowserOpen);
const toggleFile = useSettingsStore((state) => state.toggleFile);
const replaceSelectedNames = useSettingsStore((state) => state.replaceSelectedNames);
const setFileQuery = useSettingsStore((state) => state.setFileQuery);
const setFilePage = useSettingsStore((state) => state.setFilePage);
const setPageSize = useSettingsStore((state) => state.setPageSize);
const startImport = useSettingsStore((state) => state.startImport);
const filteredFiles = useMemo(() => {
const query = fileQuery.trim().toLowerCase();
if (!query) {
return remoteFiles;
}
return remoteFiles.filter((item) => {
return item.email.toLowerCase().includes(query) || item.name.toLowerCase().includes(query);
});
}, [fileQuery, remoteFiles]);
const currentPageSize = Number(pageSize);
const filePageCount = Math.max(1, Math.ceil(filteredFiles.length / currentPageSize));
const safeFilePage = Math.min(filePage, filePageCount);
const pagedFiles = filteredFiles.slice((safeFilePage - 1) * currentPageSize, safeFilePage * currentPageSize);
const allFilteredSelected = filteredFiles.length > 0 && filteredFiles.every((item) => selectedNames.includes(item.name));
const toggleSelectAllFiltered = (checked: boolean) => {
if (checked) {
replaceSelectedNames([...selectedNames, ...filteredFiles.map((item) => item.name)]);
return;
}
const filteredSet = new Set(filteredFiles.map((item) => item.name));
replaceSelectedNames(selectedNames.filter((name) => !filteredSet.has(name)));
};
return (
<Dialog open={browserOpen} onOpenChange={setBrowserOpen}>
<DialogContent showCloseButton={false} className="max-h-[90vh] max-w-5xl rounded-2xl p-6">
<DialogHeader className="gap-2">
<DialogTitle>选择要导入的账号</DialogTitle>
<DialogDescription className="text-sm leading-6">
{browserPool ? `来自 ${browserPool.name || browserPool.base_url}` : "读取到的远程账号列表"}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="relative min-w-[260px]">
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-stone-400" />
<Input
value={fileQuery}
onChange={(event) => setFileQuery(event.target.value)}
placeholder="搜索 email 或文件名"
className="h-10 rounded-xl border-stone-200 bg-white pl-10"
/>
</div>
<div className="flex items-center gap-2">
<Select value={pageSize} onValueChange={(value) => setPageSize(value as (typeof PAGE_SIZE_OPTIONS)[number])}>
<SelectTrigger className="h-10 w-[120px] rounded-xl border-stone-200 bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PAGE_SIZE_OPTIONS.map((item) => (
<SelectItem key={item} value={item}>
{item} / 页
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
className="h-10 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
onClick={() => toggleSelectAllFiltered(!allFilteredSelected)}
>
{allFilteredSelected ? "取消全选" : "全选筛选结果"}
</Button>
</div>
</div>
<div className="rounded-xl border border-stone-200">
<div className="flex items-center justify-between border-b border-stone-100 px-4 py-3 text-sm text-stone-500">
<div className="flex items-center gap-3">
<Checkbox checked={allFilteredSelected} onCheckedChange={(checked) => toggleSelectAllFiltered(Boolean(checked))} />
<span>筛选结果 {filteredFiles.length} 个</span>
</div>
<span>已选 {selectedNames.length} 个</span>
</div>
<div className="max-h-[420px] overflow-auto">
{pagedFiles.length === 0 ? (
<div className="flex items-center justify-center py-12 text-sm text-stone-400">没有匹配的远程账号</div>
) : (
<div className="divide-y divide-stone-100">
{pagedFiles.map((item) => (
<label key={item.name} className="flex cursor-pointer items-center gap-3 px-4 py-3 hover:bg-stone-50">
<Checkbox
checked={selectedNames.includes(item.name)}
onCheckedChange={(checked) => toggleFile(item.name, Boolean(checked))}
/>
<div className="min-w-0">
<div className="truncate text-sm font-medium text-stone-700">{item.email || item.name}</div>
<div className="truncate text-xs text-stone-400">{item.name}</div>
</div>
</label>
))}
</div>
)}
</div>
</div>
<div className="flex items-center justify-between text-sm text-stone-500">
<span>
第 {filteredFiles.length === 0 ? 0 : (safeFilePage - 1) * currentPageSize + 1} -{" "}
{Math.min(safeFilePage * currentPageSize, filteredFiles.length)} 条,共 {filteredFiles.length} 条
</span>
<div className="flex items-center gap-2">
<Button
variant="outline"
className="h-9 rounded-xl border-stone-200 bg-white px-3"
onClick={() => setFilePage(Math.max(1, safeFilePage - 1))}
disabled={safeFilePage <= 1}
>
上一页
</Button>
<span>
{safeFilePage}/{filePageCount}
</span>
<Button
variant="outline"
className="h-9 rounded-xl border-stone-200 bg-white px-3"
onClick={() => setFilePage(Math.min(filePageCount, safeFilePage + 1))}
disabled={safeFilePage >= filePageCount}
>
下一页
</Button>
</div>
</div>
<DialogFooter className="pt-2">
<Button
variant="secondary"
className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
onClick={() => setBrowserOpen(false)}
disabled={isStartingImport}
>
取消
</Button>
<Button
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
onClick={() => void startImport()}
disabled={isStartingImport || selectedNames.length === 0}
>
{isStartingImport ? <LoaderCircle className="size-4 animate-spin" /> : <Import className="size-4" />}
导入选中账号
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}