mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-21 15:11:08 +08:00
Fix: table parser metadata (#15127)
### What problem does this PR solve? This PR improves the table upload flow for CSV/Excel files by allowing table column role configuration at upload time. Previously, users had to: 1. Upload and parse a table file. 2. Open parser settings and manually set table column roles. 3. Re-parse the file for the roles to take effect. This was inefficient and required an unnecessary second parse. With this change: 1. When the knowledge base uses table parsing, the upload dialog extracts CSV/Excel headers client-side. 2. Users can choose Auto mode or Manual mode. 3. In Manual mode, users can assign per-column roles before upload. 4. The selected parser config is sent with the upload request and applied server-side during document creation. Result: configured table column roles are applied from the first parse. ### Type of change - [x] New Feature (non-breaking change which adds functionality) Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
This commit is contained in:
@@ -6,17 +6,42 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { extractTableColumns, isTableFile } from '@/utils/table-column-extract';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { TFunction } from 'i18next';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { FileUploader } from '../file-uploader';
|
||||
import { RAGFlowFormItem } from '../ragflow-form';
|
||||
import { Form } from '../ui/form';
|
||||
import { Label } from '../ui/label';
|
||||
import { RadioGroup, RadioGroupItem } from '../ui/radio-group';
|
||||
import { Switch } from '../ui/switch';
|
||||
|
||||
const ROLE_OPTIONS = [
|
||||
{ value: 'both', labelKey: 'knowledgeConfiguration.tableColumnRoleBoth' },
|
||||
{
|
||||
value: 'indexing',
|
||||
labelKey: 'knowledgeConfiguration.tableColumnRoleIndexing',
|
||||
},
|
||||
{
|
||||
value: 'metadata',
|
||||
labelKey: 'knowledgeConfiguration.tableColumnRoleMetadata',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type TableColumnRoles = Record<string, 'indexing' | 'metadata' | 'both'>;
|
||||
|
||||
function buildUploadFormSchema(t: TFunction) {
|
||||
const FormSchema = z.object({
|
||||
parseOnCreation: z.boolean().optional(),
|
||||
@@ -31,6 +56,10 @@ function buildUploadFormSchema(t: TFunction) {
|
||||
),
|
||||
)
|
||||
.min(1, { message: t('fileManager.pleaseUploadAtLeastOneFile') }),
|
||||
tableColumnMode: z.enum(['auto', 'manual']).optional(),
|
||||
tableColumnRoles: z
|
||||
.record(z.enum(['indexing', 'metadata', 'both']))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
return FormSchema;
|
||||
@@ -45,8 +74,13 @@ const UploadFormId = 'UploadFormId';
|
||||
type UploadFormProps = {
|
||||
submit: (values?: UploadFormSchemaType) => void;
|
||||
showParseOnCreation?: boolean;
|
||||
isTableParser?: boolean;
|
||||
};
|
||||
function UploadForm({ submit, showParseOnCreation }: UploadFormProps) {
|
||||
function UploadForm({
|
||||
submit,
|
||||
showParseOnCreation,
|
||||
isTableParser,
|
||||
}: UploadFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const FormSchema = buildUploadFormSchema(t);
|
||||
|
||||
@@ -56,9 +90,64 @@ function UploadForm({ submit, showParseOnCreation }: UploadFormProps) {
|
||||
defaultValues: {
|
||||
parseOnCreation: false,
|
||||
fileList: [],
|
||||
tableColumnMode: 'auto',
|
||||
tableColumnRoles: {},
|
||||
},
|
||||
});
|
||||
|
||||
const [extractedColumns, setExtractedColumns] = useState<string[]>([]);
|
||||
const [columnMode, setColumnMode] = useState<'auto' | 'manual'>('auto');
|
||||
const [columnRoles, setColumnRoles] = useState<TableColumnRoles>({});
|
||||
|
||||
const handleFilesChange = useCallback(
|
||||
async (files: any[]) => {
|
||||
if (!isTableParser || !files || files.length === 0) {
|
||||
setExtractedColumns([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract columns from the first table file
|
||||
const allColumns = new Set<string>();
|
||||
for (const f of files) {
|
||||
const file = f instanceof File ? f : f.file;
|
||||
if (file && isTableFile(file)) {
|
||||
const cols = await extractTableColumns(file);
|
||||
cols.forEach((c) => allColumns.add(c));
|
||||
}
|
||||
}
|
||||
setExtractedColumns(Array.from(allColumns));
|
||||
},
|
||||
[isTableParser],
|
||||
);
|
||||
|
||||
const handleModeChange = (value: 'auto' | 'manual') => {
|
||||
setColumnMode(value);
|
||||
form.setValue('tableColumnMode', value);
|
||||
};
|
||||
|
||||
const handleRoleChange = (col: string, role: string) => {
|
||||
const updated = {
|
||||
...columnRoles,
|
||||
[col]: role as 'indexing' | 'metadata' | 'both',
|
||||
};
|
||||
setColumnRoles(updated);
|
||||
form.setValue('tableColumnRoles', updated);
|
||||
};
|
||||
|
||||
// Sync column roles to form when columns are extracted
|
||||
useEffect(() => {
|
||||
if (columnMode === 'manual' && extractedColumns.length > 0) {
|
||||
const roles: TableColumnRoles = {};
|
||||
extractedColumns.forEach((col) => {
|
||||
roles[col] = columnRoles[col] || 'both';
|
||||
});
|
||||
setColumnRoles(roles);
|
||||
form.setValue('tableColumnRoles', roles);
|
||||
}
|
||||
}, [extractedColumns, columnMode]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const showColumnConfig = isTableParser && extractedColumns.length > 0;
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -84,47 +173,117 @@ function UploadForm({ submit, showParseOnCreation }: UploadFormProps) {
|
||||
{(field) => (
|
||||
<FileUploader
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
onValueChange={(files) => {
|
||||
field.onChange(files);
|
||||
handleFilesChange(files);
|
||||
}}
|
||||
accept={{}}
|
||||
data-testid="dataset-upload-dropzone"
|
||||
/>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
|
||||
{showColumnConfig && (
|
||||
<div className="space-y-3 border rounded-md p-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">
|
||||
{t('knowledgeConfiguration.tableColumnMode')}
|
||||
</Label>
|
||||
<RadioGroup
|
||||
value={columnMode}
|
||||
onValueChange={handleModeChange}
|
||||
className="flex gap-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="auto" id="upload-mode-auto" />
|
||||
<label
|
||||
htmlFor="upload-mode-auto"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
{t('knowledgeConfiguration.tableColumnModeAuto')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="manual" id="upload-mode-manual" />
|
||||
<label
|
||||
htmlFor="upload-mode-manual"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
{t('knowledgeConfiguration.tableColumnModeManual')}
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{columnMode === 'auto' && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('knowledgeConfiguration.tableColumnModeAutoDescription')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{columnMode === 'manual' && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('knowledgeConfiguration.tableColumnRolesTip')}
|
||||
</p>
|
||||
<div className="space-y-2 max-h-[200px] overflow-y-auto">
|
||||
{extractedColumns.map((col) => (
|
||||
<div key={col} className="flex items-center gap-3">
|
||||
<Label className="min-w-[120px] shrink-0 text-sm font-normal truncate">
|
||||
{col}
|
||||
</Label>
|
||||
<Select
|
||||
value={columnRoles[col] || 'both'}
|
||||
onValueChange={(value) => handleRoleChange(col, value)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ROLE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{t(opt.labelKey)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
type FileUploadDialogProps = IModalProps<UploadFormSchemaType> &
|
||||
Pick<UploadFormProps, 'showParseOnCreation'>;
|
||||
Pick<UploadFormProps, 'showParseOnCreation' | 'isTableParser'>;
|
||||
export function FileUploadDialog({
|
||||
hideModal,
|
||||
onOk,
|
||||
loading,
|
||||
showParseOnCreation = false,
|
||||
isTableParser = false,
|
||||
}: FileUploadDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent data-testid="dataset-upload-modal">
|
||||
<DialogContent
|
||||
data-testid="dataset-upload-modal"
|
||||
className="max-h-[85vh] overflow-y-auto"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('fileManager.uploadFile')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{/* <Tabs defaultValue="account">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-4">
|
||||
<TabsTrigger value="account">{t('fileManager.local')}</TabsTrigger>
|
||||
<TabsTrigger value="password">{t('fileManager.s3')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="account">
|
||||
<UploadForm
|
||||
submit={onOk!}
|
||||
showParseOnCreation={showParseOnCreation}
|
||||
></UploadForm>
|
||||
</TabsContent>
|
||||
<TabsContent value="password">{t('common.comingSoon')}</TabsContent>
|
||||
</Tabs> */}
|
||||
<UploadForm submit={onOk!} showParseOnCreation={showParseOnCreation} />
|
||||
<UploadForm
|
||||
submit={onOk!}
|
||||
showParseOnCreation={showParseOnCreation}
|
||||
isTableParser={isTableParser}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<ButtonLoading type="submit" loading={loading} form={UploadFormId}>
|
||||
{t('common.save')}
|
||||
|
||||
Reference in New Issue
Block a user