refactor: enhance seed configuration handling with added fields, dynamic chunking logic, and streamlined interactions
This commit is contained in:
parent
424b00b701
commit
71ab9ff4b4
10 changed files with 367 additions and 93 deletions
|
|
@ -4,6 +4,8 @@ import {
|
|||
CodeIcon,
|
||||
CodeSimpleIcon,
|
||||
DiceFaces03Icon,
|
||||
DocumentAttachmentIcon,
|
||||
DocumentCodeIcon,
|
||||
EqualSignIcon,
|
||||
FingerPrintIcon,
|
||||
FunctionIcon,
|
||||
|
|
@ -116,7 +118,7 @@ const BLOCK_DEFINITIONS: BlockDefinition[] = [
|
|||
type: "seed_local",
|
||||
title: "Seed (Local File)",
|
||||
description: "Upload CSV/JSON/JSONL and use rows as seed context.",
|
||||
icon: Plant01Icon,
|
||||
icon: DocumentCodeIcon,
|
||||
dialogKey: "seed",
|
||||
createConfig: (id, existing) => makeSeedConfig(id, existing, "local"),
|
||||
},
|
||||
|
|
@ -125,7 +127,7 @@ const BLOCK_DEFINITIONS: BlockDefinition[] = [
|
|||
type: "seed_unstructured",
|
||||
title: "Seed (Unstructured)",
|
||||
description: "Upload PDF/DOCX/TXT, chunk to text rows, then seed.",
|
||||
icon: Plant01Icon,
|
||||
icon: DocumentAttachmentIcon,
|
||||
dialogKey: "seed",
|
||||
createConfig: (id, existing) => makeSeedConfig(id, existing, "unstructured"),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ export function ConfigDialog({
|
|||
)}
|
||||
{renderBlockDialog(
|
||||
config,
|
||||
open,
|
||||
categoryOptions,
|
||||
modelConfigAliases,
|
||||
modelProviderOptions,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import {
|
|||
} from "@/components/ui/tabs";
|
||||
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
|
||||
import mammoth from "mammoth";
|
||||
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
||||
import { type ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { extractText, getDocumentProxy } from "unpdf";
|
||||
import { inspectSeedDataset, inspectSeedUpload } from "../../api";
|
||||
import type {
|
||||
|
|
@ -59,16 +59,14 @@ const SELECTION_OPTIONS: Array<{ value: SeedSelectionType; label: string }> = [
|
|||
const LOCAL_ACCEPT = ".csv,.json,.jsonl";
|
||||
const UNSTRUCTURED_ACCEPT = ".txt,.pdf,.docx";
|
||||
const MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
||||
const CHUNK_SIZE = 1200;
|
||||
const CHUNK_OVERLAP = 200;
|
||||
const UNSTRUCTURED_SPLITTER = new RecursiveCharacterTextSplitter({
|
||||
chunkSize: CHUNK_SIZE,
|
||||
chunkOverlap: CHUNK_OVERLAP,
|
||||
});
|
||||
const DEFAULT_CHUNK_SIZE = 1200;
|
||||
const DEFAULT_CHUNK_OVERLAP = 200;
|
||||
const MAX_CHUNK_SIZE = 20000;
|
||||
|
||||
type SeedDialogProps = {
|
||||
config: SeedConfig;
|
||||
onUpdate: (patch: Partial<SeedConfig>) => void;
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string): string {
|
||||
|
|
@ -89,11 +87,54 @@ function stringifyCell(value: unknown): string {
|
|||
}
|
||||
}
|
||||
|
||||
async function chunkText(input: string): Promise<string[]> {
|
||||
function parseChunkNumber(
|
||||
value: string | undefined,
|
||||
fallback: number,
|
||||
min: number,
|
||||
max: number,
|
||||
): number {
|
||||
const raw = value?.trim();
|
||||
if (!raw) return fallback;
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
const int = Math.floor(parsed);
|
||||
if (int < min) return min;
|
||||
if (int > max) return max;
|
||||
return int;
|
||||
}
|
||||
|
||||
function resolveChunking(config: SeedConfig): {
|
||||
chunkSize: number;
|
||||
chunkOverlap: number;
|
||||
} {
|
||||
const chunkSize = parseChunkNumber(
|
||||
config.unstructured_chunk_size,
|
||||
DEFAULT_CHUNK_SIZE,
|
||||
1,
|
||||
MAX_CHUNK_SIZE,
|
||||
);
|
||||
const chunkOverlap = parseChunkNumber(
|
||||
config.unstructured_chunk_overlap,
|
||||
DEFAULT_CHUNK_OVERLAP,
|
||||
0,
|
||||
Math.max(0, chunkSize - 1),
|
||||
);
|
||||
return { chunkSize, chunkOverlap };
|
||||
}
|
||||
|
||||
async function chunkText(
|
||||
input: string,
|
||||
chunkSize: number,
|
||||
chunkOverlap: number,
|
||||
): Promise<string[]> {
|
||||
const text = input.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
||||
if (!text) return [];
|
||||
|
||||
const chunks = await UNSTRUCTURED_SPLITTER.splitText(text);
|
||||
const splitter = new RecursiveCharacterTextSplitter({
|
||||
chunkSize,
|
||||
chunkOverlap,
|
||||
});
|
||||
const chunks = await splitter.splitText(text);
|
||||
return chunks.map((chunk) => chunk.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +170,7 @@ async function extractUnstructuredText(file: File): Promise<string> {
|
|||
throw new Error("Unsupported unstructured file type");
|
||||
}
|
||||
|
||||
export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement {
|
||||
export function SeedDialog({ config, onUpdate, open }: SeedDialogProps): ReactElement {
|
||||
const [inspectError, setInspectError] = useState<string | null>(null);
|
||||
const [isInspecting, setIsInspecting] = useState(false);
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
|
|
@ -141,20 +182,53 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
|
||||
useEffect(() => {
|
||||
setInspectError(null);
|
||||
setPreviewRows([]);
|
||||
setLocalFile(null);
|
||||
setUnstructuredFile(null);
|
||||
}, [mode]);
|
||||
|
||||
useEffect(() => {
|
||||
setPreviewRows(config.seed_preview_rows ?? []);
|
||||
}, [config.seed_preview_rows]);
|
||||
|
||||
const samplingId = `${config.id}-sampling`;
|
||||
const selectionId = `${config.id}-selection`;
|
||||
const tokenId = `${config.id}-hf-token`;
|
||||
const subsetId = `${config.id}-hf-subset`;
|
||||
const splitId = `${config.id}-hf-split`;
|
||||
const datasetId = `${config.id}-hf-dataset`;
|
||||
const chunkSizeId = `${config.id}-chunk-size`;
|
||||
const chunkOverlapId = `${config.id}-chunk-overlap`;
|
||||
const [lastLoadedKey, setLastLoadedKey] = useState<string | null>(null);
|
||||
const wasOpenRef = useRef(open);
|
||||
|
||||
async function loadSeedMetadata(): Promise<void> {
|
||||
setInspectError(null);
|
||||
const getCurrentLoadKey = useCallback((): string | null => {
|
||||
if (mode === "hf") {
|
||||
const dataset = config.hf_repo_id.trim();
|
||||
if (!dataset) return null;
|
||||
const subset = config.hf_subset?.trim() ?? "";
|
||||
const split = config.hf_split?.trim() || "train";
|
||||
const token = config.hf_token?.trim() ?? "";
|
||||
return `hf:${dataset}|${subset}|${split}|${token}`;
|
||||
}
|
||||
if (mode === "local") {
|
||||
if (!localFile) return null;
|
||||
return `local:${localFile.name}|${localFile.size}|${localFile.lastModified}`;
|
||||
}
|
||||
if (!unstructuredFile) return null;
|
||||
const { chunkSize, chunkOverlap } = resolveChunking(config);
|
||||
return `unstructured:${unstructuredFile.name}|${unstructuredFile.size}|${unstructuredFile.lastModified}|${chunkSize}|${chunkOverlap}`;
|
||||
}, [
|
||||
config,
|
||||
localFile,
|
||||
mode,
|
||||
unstructuredFile,
|
||||
]);
|
||||
|
||||
const loadSeedMetadata = useCallback(async (opts?: { silent?: boolean }): Promise<boolean> => {
|
||||
const loadKey = getCurrentLoadKey();
|
||||
if (!opts?.silent) {
|
||||
setInspectError(null);
|
||||
}
|
||||
setIsInspecting(true);
|
||||
try {
|
||||
if (mode === "hf") {
|
||||
|
|
@ -172,13 +246,15 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
onUpdate({
|
||||
hf_path: response.resolved_path,
|
||||
seed_columns: response.columns,
|
||||
seed_preview_rows: response.preview_rows ?? [],
|
||||
hf_split: response.split ?? config.hf_split ?? "",
|
||||
hf_subset: response.subset ?? config.hf_subset ?? "",
|
||||
local_file_name: "",
|
||||
unstructured_file_name: "",
|
||||
});
|
||||
setPreviewRows(response.preview_rows ?? []);
|
||||
return;
|
||||
setLastLoadedKey(loadKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mode === "local") {
|
||||
|
|
@ -197,6 +273,7 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
onUpdate({
|
||||
hf_path: response.resolved_path,
|
||||
seed_columns: response.columns,
|
||||
seed_preview_rows: response.preview_rows ?? [],
|
||||
hf_repo_id: "",
|
||||
hf_subset: "",
|
||||
hf_split: "",
|
||||
|
|
@ -204,7 +281,8 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
unstructured_file_name: "",
|
||||
});
|
||||
setPreviewRows(response.preview_rows ?? []);
|
||||
return;
|
||||
setLastLoadedKey(loadKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!unstructuredFile) {
|
||||
|
|
@ -215,7 +293,8 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
}
|
||||
|
||||
const text = await extractUnstructuredText(unstructuredFile);
|
||||
const chunks = await chunkText(text);
|
||||
const { chunkSize, chunkOverlap } = resolveChunking(config);
|
||||
const chunks = await chunkText(text, chunkSize, chunkOverlap);
|
||||
if (chunks.length === 0) {
|
||||
throw new Error("No text found in file.");
|
||||
}
|
||||
|
|
@ -238,6 +317,7 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
onUpdate({
|
||||
hf_path: response.resolved_path,
|
||||
seed_columns: response.columns,
|
||||
seed_preview_rows: response.preview_rows ?? [],
|
||||
hf_repo_id: "",
|
||||
hf_subset: "",
|
||||
hf_split: "",
|
||||
|
|
@ -245,13 +325,38 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
unstructured_file_name: unstructuredFile.name,
|
||||
});
|
||||
setPreviewRows(response.preview_rows ?? []);
|
||||
setLastLoadedKey(loadKey);
|
||||
return true;
|
||||
} catch (error) {
|
||||
setInspectError(getErrorMessage(error, "Failed to load seed metadata."));
|
||||
if (!opts?.silent) {
|
||||
setInspectError(getErrorMessage(error, "Failed to load seed metadata."));
|
||||
}
|
||||
setPreviewRows([]);
|
||||
return false;
|
||||
} finally {
|
||||
setIsInspecting(false);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
config,
|
||||
getCurrentLoadKey,
|
||||
localFile,
|
||||
mode,
|
||||
onUpdate,
|
||||
unstructuredFile,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const wasOpen = wasOpenRef.current;
|
||||
wasOpenRef.current = open;
|
||||
if (!wasOpen || open || isInspecting) {
|
||||
return;
|
||||
}
|
||||
const key = getCurrentLoadKey();
|
||||
if (!key || key === lastLoadedKey) {
|
||||
return;
|
||||
}
|
||||
void loadSeedMetadata({ silent: true });
|
||||
}, [getCurrentLoadKey, isInspecting, lastLoadedKey, loadSeedMetadata, open]);
|
||||
|
||||
const previewColumns = useMemo(() => {
|
||||
const loadedColumns = config.seed_columns ?? [];
|
||||
|
|
@ -260,13 +365,6 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
return [];
|
||||
}, [config.seed_columns, previewRows]);
|
||||
|
||||
const canLoad =
|
||||
mode === "hf"
|
||||
? Boolean(config.hf_repo_id.trim())
|
||||
: mode === "local"
|
||||
? Boolean(localFile)
|
||||
: Boolean(unstructuredFile);
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="config" className="w-full">
|
||||
<TabsList className="w-full">
|
||||
|
|
@ -284,19 +382,31 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
htmlFor={datasetId}
|
||||
hint="Hugging Face dataset repo id (org/repo)."
|
||||
/>
|
||||
<Input
|
||||
id={datasetId}
|
||||
className="nodrag"
|
||||
placeholder="org/repo"
|
||||
value={config.hf_repo_id}
|
||||
onChange={(event) =>
|
||||
onUpdate({
|
||||
hf_repo_id: event.target.value,
|
||||
hf_path: "",
|
||||
seed_columns: [],
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id={datasetId}
|
||||
className="nodrag flex-1"
|
||||
placeholder="org/repo"
|
||||
value={config.hf_repo_id}
|
||||
onChange={(event) =>
|
||||
onUpdate({
|
||||
hf_repo_id: event.target.value,
|
||||
hf_path: "",
|
||||
seed_columns: [],
|
||||
seed_preview_rows: [],
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="nodrag shrink-0"
|
||||
onClick={() => void loadSeedMetadata()}
|
||||
disabled={isInspecting || !config.hf_repo_id.trim()}
|
||||
>
|
||||
{isInspecting ? "Loading..." : "Load"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
|
|
@ -353,21 +463,39 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
label="Local file"
|
||||
hint="Upload CSV, JSON, or JSONL seed file."
|
||||
/>
|
||||
<Input
|
||||
className="nodrag"
|
||||
type="file"
|
||||
accept={LOCAL_ACCEPT}
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0] ?? null;
|
||||
setLocalFile(file);
|
||||
onUpdate({ hf_path: "", seed_columns: [] });
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
className="nodrag flex-1"
|
||||
type="file"
|
||||
accept={LOCAL_ACCEPT}
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0] ?? null;
|
||||
setLocalFile(file);
|
||||
onUpdate({
|
||||
hf_path: "",
|
||||
seed_columns: [],
|
||||
seed_preview_rows: [],
|
||||
local_file_name: file?.name ?? "",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="nodrag shrink-0"
|
||||
onClick={() => void loadSeedMetadata()}
|
||||
disabled={isInspecting || !localFile}
|
||||
>
|
||||
{isInspecting ? "Loading..." : "Load"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Upload-only. Max 50MB.
|
||||
</p>
|
||||
{localFile && (
|
||||
<p className="text-xs text-muted-foreground">Selected: {localFile.name}</p>
|
||||
{(localFile?.name || config.local_file_name?.trim()) && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Selected: {localFile?.name ?? config.local_file_name?.trim()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -378,37 +506,45 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
label="Unstructured file"
|
||||
hint="Upload PDF, DOCX, or TXT. We chunk text into seed rows."
|
||||
/>
|
||||
<Input
|
||||
className="nodrag"
|
||||
type="file"
|
||||
accept={UNSTRUCTURED_ACCEPT}
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0] ?? null;
|
||||
setUnstructuredFile(file);
|
||||
onUpdate({ hf_path: "", seed_columns: [] });
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
className="nodrag flex-1"
|
||||
type="file"
|
||||
accept={UNSTRUCTURED_ACCEPT}
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0] ?? null;
|
||||
setUnstructuredFile(file);
|
||||
onUpdate({
|
||||
hf_path: "",
|
||||
seed_columns: [],
|
||||
seed_preview_rows: [],
|
||||
unstructured_file_name: file?.name ?? "",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="nodrag shrink-0"
|
||||
onClick={() => void loadSeedMetadata()}
|
||||
disabled={isInspecting || !unstructuredFile}
|
||||
>
|
||||
{isInspecting ? "Loading..." : "Load"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Chunking uses chunk_text only. Max 50MB.
|
||||
</p>
|
||||
{unstructuredFile && (
|
||||
{(unstructuredFile?.name ||
|
||||
config.unstructured_file_name?.trim()) && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Selected: {unstructuredFile.name}
|
||||
Selected:{" "}
|
||||
{unstructuredFile?.name ?? config.unstructured_file_name?.trim()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="nodrag"
|
||||
onClick={() => void loadSeedMetadata()}
|
||||
disabled={isInspecting || !canLoad}
|
||||
>
|
||||
{isInspecting ? "Loading..." : "Load columns + 10 rows"}
|
||||
</Button>
|
||||
|
||||
{inspectError && <p className="text-xs text-red-600">{inspectError}</p>}
|
||||
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
|
|
@ -472,6 +608,46 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
</Select>
|
||||
</div>
|
||||
|
||||
{mode === "unstructured" && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Chunk size"
|
||||
htmlFor={chunkSizeId}
|
||||
hint="Characters per chunk."
|
||||
/>
|
||||
<Input
|
||||
id={chunkSizeId}
|
||||
className="nodrag"
|
||||
inputMode="numeric"
|
||||
value={config.unstructured_chunk_size ?? String(DEFAULT_CHUNK_SIZE)}
|
||||
onChange={(event) =>
|
||||
onUpdate({ unstructured_chunk_size: event.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Chunk overlap"
|
||||
htmlFor={chunkOverlapId}
|
||||
hint="Shared chars between adjacent chunks."
|
||||
/>
|
||||
<Input
|
||||
id={chunkOverlapId}
|
||||
className="nodrag"
|
||||
inputMode="numeric"
|
||||
value={
|
||||
config.unstructured_chunk_overlap ??
|
||||
String(DEFAULT_CHUNK_OVERLAP)
|
||||
}
|
||||
onChange={(event) =>
|
||||
onUpdate({ unstructured_chunk_overlap: event.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.selection_type === "index_range" && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-2">
|
||||
|
|
@ -532,20 +708,10 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
<EmptyHeader>
|
||||
<EmptyTitle>Seed preview</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Click load to fetch 10 rows from the selected seed source.
|
||||
Use the load button next to the source input to fetch 10 rows.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="nodrag"
|
||||
onClick={() => void loadSeedMetadata()}
|
||||
disabled={isInspecting || !canLoad}
|
||||
>
|
||||
{isInspecting ? "Loading..." : "Load 10 rows"}
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
<EmptyContent />
|
||||
</Empty>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -553,8 +719,8 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
<div className="text-xs text-muted-foreground">
|
||||
Loaded columns: {previewColumns.join(", ") || "None"}
|
||||
</div>
|
||||
<div className="max-h-[360px] overflow-auto rounded-xl border border-border/60">
|
||||
<Table>
|
||||
<div className="max-h-[360px] overflow-auto rounded-xl corner-squircle border border-border/60">
|
||||
<Table className="corner-squircle">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{previewColumns.map((col) => (
|
||||
|
|
|
|||
|
|
@ -319,6 +319,9 @@ export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
|
|||
local_file_name: "",
|
||||
unstructured_file_name: "",
|
||||
seed_columns: [],
|
||||
seed_preview_rows: [],
|
||||
unstructured_chunk_size: "1200",
|
||||
unstructured_chunk_overlap: "200",
|
||||
};
|
||||
return {
|
||||
configs: {
|
||||
|
|
|
|||
|
|
@ -230,6 +230,12 @@ export type SeedConfig = {
|
|||
hf_endpoint?: string;
|
||||
local_file_name?: string;
|
||||
unstructured_file_name?: string;
|
||||
// ui-only
|
||||
seed_preview_rows?: Record<string, unknown>[];
|
||||
// ui-only (string for input ergonomics)
|
||||
unstructured_chunk_size?: string;
|
||||
// ui-only (string for input ergonomics)
|
||||
unstructured_chunk_overlap?: string;
|
||||
seed_splits?: string[];
|
||||
// ui-only
|
||||
// biome-ignore lint/style/useNamingConvention: ui schema
|
||||
|
|
|
|||
|
|
@ -296,6 +296,9 @@ export function makeSeedConfig(
|
|||
hf_endpoint: "https://huggingface.co",
|
||||
local_file_name: "",
|
||||
unstructured_file_name: "",
|
||||
seed_preview_rows: [],
|
||||
unstructured_chunk_size: "1200",
|
||||
unstructured_chunk_overlap: "200",
|
||||
seed_splits: [],
|
||||
seed_globs_by_split: {},
|
||||
seed_columns: [],
|
||||
|
|
|
|||
|
|
@ -31,8 +31,24 @@ type UiInput = {
|
|||
nodes?: unknown;
|
||||
edges?: unknown;
|
||||
seed_source_type?: unknown;
|
||||
seed_columns?: unknown;
|
||||
seed_preview_rows?: unknown;
|
||||
local_file_name?: unknown;
|
||||
unstructured_file_name?: unknown;
|
||||
unstructured_chunk_size?: unknown;
|
||||
unstructured_chunk_overlap?: unknown;
|
||||
};
|
||||
|
||||
function readStringNumber(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return String(value);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseProcessors(input: unknown): RecipeProcessorConfig[] {
|
||||
if (!Array.isArray(input)) {
|
||||
return [];
|
||||
|
|
@ -228,11 +244,36 @@ export function importRecipePayload(input: string): ImportResult {
|
|||
uiSeedSourceTypeRaw === "unstructured"
|
||||
? uiSeedSourceTypeRaw
|
||||
: undefined;
|
||||
const uiSeedColumns = Array.isArray(ui?.seed_columns)
|
||||
? ui.seed_columns
|
||||
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
||||
.filter(Boolean)
|
||||
: undefined;
|
||||
const uiSeedPreviewRows = Array.isArray(ui?.seed_preview_rows)
|
||||
? ui.seed_preview_rows
|
||||
.filter((row): row is Record<string, unknown> => isRecord(row))
|
||||
.map((row) => ({ ...row }))
|
||||
: undefined;
|
||||
const uiLocalFileName = readString(ui?.local_file_name) ?? undefined;
|
||||
const uiUnstructuredFileName =
|
||||
readString(ui?.unstructured_file_name) ?? undefined;
|
||||
const uiUnstructuredChunkSize = readStringNumber(ui?.unstructured_chunk_size);
|
||||
const uiUnstructuredChunkOverlap = readStringNumber(
|
||||
ui?.unstructured_chunk_overlap,
|
||||
);
|
||||
|
||||
if (recipe.seed_config) {
|
||||
const id = `n${nextId}`;
|
||||
nextId += 1;
|
||||
const seedConfig = parseSeedConfig(recipe.seed_config, id, uiSeedSourceType);
|
||||
const seedConfig = parseSeedConfig(recipe.seed_config, id, {
|
||||
preferredSourceType: uiSeedSourceType,
|
||||
seed_columns: uiSeedColumns,
|
||||
seed_preview_rows: uiSeedPreviewRows,
|
||||
local_file_name: uiLocalFileName,
|
||||
unstructured_file_name: uiUnstructuredFileName,
|
||||
unstructured_chunk_size: uiUnstructuredChunkSize,
|
||||
unstructured_chunk_overlap: uiUnstructuredChunkOverlap,
|
||||
});
|
||||
if (seedConfig) {
|
||||
configs.push(seedConfig);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ function makeDefaultSeedConfig(id: string): SeedConfig {
|
|||
hf_endpoint: "https://huggingface.co",
|
||||
local_file_name: "",
|
||||
unstructured_file_name: "",
|
||||
seed_preview_rows: [],
|
||||
unstructured_chunk_size: "1200",
|
||||
unstructured_chunk_overlap: "200",
|
||||
seed_splits: [],
|
||||
seed_globs_by_split: {},
|
||||
seed_columns: [],
|
||||
|
|
@ -127,7 +130,15 @@ function parseSeedSettings(seedConfigRaw: unknown): Partial<SeedConfig> {
|
|||
export function parseSeedConfig(
|
||||
seedConfigRaw: unknown,
|
||||
id: string,
|
||||
preferredSourceType?: SeedSourceType,
|
||||
options?: {
|
||||
preferredSourceType?: SeedSourceType;
|
||||
seed_columns?: string[];
|
||||
seed_preview_rows?: Record<string, unknown>[];
|
||||
local_file_name?: string;
|
||||
unstructured_file_name?: string;
|
||||
unstructured_chunk_size?: string;
|
||||
unstructured_chunk_overlap?: string;
|
||||
},
|
||||
): SeedConfig | null {
|
||||
if (!seedConfigRaw) {
|
||||
return null;
|
||||
|
|
@ -136,8 +147,8 @@ export function parseSeedConfig(
|
|||
let sourceType: SeedSourceType = "hf";
|
||||
if (parsed.seed_source_type === "hf") {
|
||||
sourceType = "hf";
|
||||
} else if (preferredSourceType) {
|
||||
sourceType = preferredSourceType;
|
||||
} else if (options?.preferredSourceType) {
|
||||
sourceType = options.preferredSourceType;
|
||||
} else if (parsed.seed_source_type) {
|
||||
sourceType = parsed.seed_source_type;
|
||||
}
|
||||
|
|
@ -145,5 +156,21 @@ export function parseSeedConfig(
|
|||
...makeDefaultSeedConfig(id),
|
||||
...parsed, // payload-only fields override ui defaults
|
||||
seed_source_type: sourceType,
|
||||
...(options?.seed_columns ? { seed_columns: options.seed_columns } : {}),
|
||||
...(options?.seed_preview_rows
|
||||
? { seed_preview_rows: options.seed_preview_rows }
|
||||
: {}),
|
||||
...(options?.local_file_name !== undefined
|
||||
? { local_file_name: options.local_file_name }
|
||||
: {}),
|
||||
...(options?.unstructured_file_name !== undefined
|
||||
? { unstructured_file_name: options.unstructured_file_name }
|
||||
: {}),
|
||||
...(options?.unstructured_chunk_size !== undefined
|
||||
? { unstructured_chunk_size: options.unstructured_chunk_size }
|
||||
: {}),
|
||||
...(options?.unstructured_chunk_overlap !== undefined
|
||||
? { unstructured_chunk_overlap: options.unstructured_chunk_overlap }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,6 +242,24 @@ export function buildRecipePayload(
|
|||
nodes: uiNodes,
|
||||
edges: uiEdges,
|
||||
...(firstSeed && { seed_source_type: firstSeed.seed_source_type }),
|
||||
...(firstSeed && { seed_columns: firstSeed.seed_columns ?? [] }),
|
||||
...(firstSeed && { seed_preview_rows: firstSeed.seed_preview_rows ?? [] }),
|
||||
...(firstSeed &&
|
||||
firstSeed.local_file_name !== undefined && {
|
||||
local_file_name: firstSeed.local_file_name,
|
||||
}),
|
||||
...(firstSeed &&
|
||||
firstSeed.unstructured_file_name !== undefined && {
|
||||
unstructured_file_name: firstSeed.unstructured_file_name,
|
||||
}),
|
||||
...(firstSeed &&
|
||||
firstSeed.unstructured_chunk_size !== undefined && {
|
||||
unstructured_chunk_size: firstSeed.unstructured_chunk_size,
|
||||
}),
|
||||
...(firstSeed &&
|
||||
firstSeed.unstructured_chunk_overlap !== undefined && {
|
||||
unstructured_chunk_overlap: firstSeed.unstructured_chunk_overlap,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,6 +32,13 @@ export type RecipePayload = {
|
|||
edges: { from: string; to: string; type?: string }[];
|
||||
// ui-only, used to preserve seed block mode across imports/refresh
|
||||
seed_source_type?: "hf" | "local" | "unstructured";
|
||||
// ui-only, seed metadata cached for refresh/import UX
|
||||
seed_columns?: string[];
|
||||
seed_preview_rows?: Record<string, unknown>[];
|
||||
local_file_name?: string;
|
||||
unstructured_file_name?: string;
|
||||
unstructured_chunk_size?: string;
|
||||
unstructured_chunk_overlap?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue