import { CatalogTableToolbar, CatalogIconAction, useCatalogColumns, } from "../components/CatalogTableToolbar"; import { IconRefresh, IconDownload, IconAIFilledLevel3, IconEyeOpened, IconPlus, } from "@douyinfe/semi-icons"; import type { ColumnProps } from "@douyinfe/semi-ui-19/lib/es/table/interface"; import { WorkspaceTable as Table } from "../components/WorkspaceTable"; import { useEffect, useState } from "react"; import { Banner, Button, Input, Modal, Pagination, TextArea, Toast, Tag, } from "@douyinfe/semi-ui-19"; import { api, patch, post, queryString, displayValue, formatTime, } from "../api"; import type { Account, Job } from "../types"; import type { PageContext, UIAction } from "../ai/types"; import { ResearchSelect } from "../research/ResearchSelect"; import { FieldAvailability } from "../research/FieldAvailability"; import { ScopeControls } from "../preparations/ScopeControls"; import { FieldTable } from "../preparations/FieldTable"; import { AddFieldsDialog } from "../preparations/FieldDirectory"; import { defaultScope, scopeKey, scopeLabel, type FieldRecord, type Page, type Preparation, } from "../preparations/types"; import "../preparations/style.css"; type Entry = { id: string; name: string | null; description: string | null; category: string | null; subcategory: string | null; field_count: number | null; complete_count: number | null; collection_version: string | null; value_score: number | null; synced_at: string; research?: { note: string; version: number }; }; type Result = Page & { categories: Record; synced_at: string | null; }; export function DatasetPage({ active, version, onTask, onModal, onChat, onContext, suspended, account, }: { active: boolean; version: string; onTask: () => void; onModal: (v: boolean) => void; onChat: () => void; onContext: (c: PageContext) => void; suspended: boolean; account: Account | null; jobs: Job[]; action: UIAction | null; }) { const [scope, setScope] = useState(defaultScope), [q, setQ] = useState(""), [category, setCategory] = useState(""), [subcategory, setSubcategory] = useState(""); const [filterDraft, setFilterDraft] = useState({ q: "", category: "", subcategory: "", }); const columnSettings = useCatalogColumns("catalog-dataset-columns", [ { key: "dataset", label: "数据集", required: true }, { key: "category", label: "分类" }, { key: "field_count", label: "字段数" }, { key: "value_score", label: "价值评分" }, { key: "status", label: "同步状态" }, { key: "actions", label: "操作", required: true }, ]); const [sort, setSort] = useState("name"), [direction, setDirection] = useState("asc"), [page, setPage] = useState(1), [pageSize, setPageSize] = useState(25), [rev, setRev] = useState(0); const [data, setData] = useState({ items: [], total: 0, limit: 25, offset: 0, categories: {}, synced_at: null, }); const [detail, setDetail] = useState(null), [fields, setFields] = useState>({ items: [], total: 0, limit: 25, offset: 0, }); const [fieldQ, setFieldQ] = useState(""), [fieldPage, setFieldPage] = useState(1), [selected, setSelected] = useState>({}); const [adding, setAdding] = useState(false), [note, setNote] = useState(""), [noteVersion, setNoteVersion] = useState(1), [fieldDetail, setFieldDetail] = useState(null); const [error, setError] = useState(""), [busy, setBusy] = useState(false), [loading, setLoading] = useState(false), [copied, setCopied] = useState(null); const key = scopeKey(scope), visible = active && !suspended && !!detail; useEffect(() => { onModal(visible); return () => onModal(false); }, [visible, onModal]); useEffect(() => { if (active) onContext({ page: "datasets", catalog_scope: scope, dataset_id: detail?.id, }); }, [active, key, detail?.id, onContext]); useEffect(() => { setPage(1); setDetail(null); setSelected({}); }, [key]); useEffect(() => { if (!active) return; const c = new AbortController(); setLoading(true); api( `/catalog/datasets?${queryString({ ...scope, q, category, subcategory, sort, direction, limit: pageSize, offset: (page - 1) * pageSize })}`, { signal: c.signal }, ) .then(setData) .catch((e) => { if (!c.signal.aborted) setError(e.message); }) .finally(() => { if (!c.signal.aborted) setLoading(false); }); return () => c.abort(); }, [ active, key, q, category, subcategory, sort, direction, page, pageSize, version, rev, ]); useEffect(() => { if (!detail || !active) return; const c = new AbortController(); api>( `/catalog/fields?${queryString({ ...scope, dataset_id: detail.id, q: fieldQ, offset: (fieldPage - 1) * 25 })}`, { signal: c.signal }, ) .then(setFields) .catch((e) => { if (!c.signal.aborted) setError(e.message); }); return () => c.abort(); }, [active, detail?.id, key, fieldQ, fieldPage, version, rev]); async function work(fn: () => Promise) { setBusy(true); setError(""); try { await fn(); } catch (e) { setError((e as Error).message); } finally { setBusy(false); } } async function sync(id?: string) { await post("/catalog/sync-jobs", { scope, dataset_id: id ?? null }); onTask(); } async function view(row: Entry) { const d = await api( `/catalog/datasets/${encodeURIComponent(row.id)}?${queryString(scope)}`, ); setDetail(d); setFieldDetail(null); setNote(d.research?.note ?? ""); setNoteVersion(d.research?.version ?? 1); setFieldQ(""); setFieldPage(1); setSelected({}); } async function use(row: Entry) { const copy = await post("/data-preparations/from-dataset", { scope, dataset_id: row.id, collection_version: row.collection_version, }); setCopied(copy); Toast.success(`已复制到数据准备:${copy.name}`); } async function openField(f: FieldRecord) { const d = await api( `/catalog/datasets/${encodeURIComponent(f.dataset_id)}/fields/${encodeURIComponent(f.id)}?${queryString(scope)}`, ); setFieldDetail(f); setNote(d.research?.note ?? ""); setNoteVersion(d.research?.version ?? 1); } async function saveNote() { const suffix = fieldDetail ? `/fields/${encodeURIComponent(fieldDetail.id)}` : ""; const n = await patch<{ note: string; version: number }>( `/catalog/datasets/${encodeURIComponent(detail!.id)}${suffix}/research?${queryString(scope)}`, { note, version: noteVersion }, ); setNoteVersion(n.version); Toast.success("备注已保存"); } const columns: ColumnProps[] = [ { title: "数据集", width: 280, render: (_, r) => ( ), }, { title: "分类", dataIndex: "category", render: displayValue }, { title: "字段数", render: (_, r) => r.complete_count ?? displayValue(r.field_count), }, { title: "价值评分", dataIndex: "value_score", render: displayValue }, { title: "同步状态", render: (_, r) => ( {r.collection_version ? "已同步字段" : "待同步字段"} ), }, { title: "操作", width: 140, render: (_, r) => (
), }, ]; const visibleColumns = columns.filter((_, index) => columnSettings.visible.includes(columnSettings.choices[index].key), ); return (
{error && } {copied && ( {copied.name} 已创建{" "} 打开数据准备 } /> )} } label="数据目录" active={active && !suspended && !detail} filterCount={[q, category, subcategory].filter(Boolean).length} columns={columnSettings} filters={
} onApply={() => { setQ(filterDraft.q); setCategory(filterDraft.category); setSubcategory(filterDraft.subcategory); setPage(1); }} onReset={() => { setFilterDraft({ q: "", category: "", subcategory: "" }); setQ(""); setCategory(""); setSubcategory(""); setPage(1); }} sort={{ value: sort, direction, options: [ { value: "name", label: "名称" }, { value: "field_count", label: "字段数" }, { value: "value_score", label: "价值评分" }, ], onChange: (value, dir) => { setSort(value); setDirection(dir); setPage(1); }, }} actions={ <>