feat: fix the problem in the Operator Market frontend pages

This commit is contained in:
root
2025-12-29 11:38:47 +08:00
parent 29e4a333a9
commit 844add27ea
213 changed files with 45547 additions and 45537 deletions

View File

@@ -1,201 +1,201 @@
import { Button, App, Steps } from "antd";
import {
ArrowLeft,
CheckCircle,
Settings,
TagIcon,
Upload,
} from "lucide-react";
import { useNavigate, useParams } from "react-router";
import { useEffect, useState } from "react";
import UploadStep from "./components/UploadStep";
import ParsingStep from "./components/ParsingStep";
import ConfigureStep from "./components/ConfigureStep";
import PreviewStep from "./components/PreviewStep";
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
import {
createOperatorUsingPost,
preUploadOperatorUsingPost,
queryOperatorByIdUsingGet,
updateOperatorByIdUsingPut,
uploadOperatorChunkUsingPost,
uploadOperatorUsingPost,
} from "../operator.api";
import { sliceFile } from "@/utils/file.util";
export default function OperatorPluginCreate() {
const navigate = useNavigate();
const { id } = useParams();
const { message } = App.useApp();
const [uploadStep, setUploadStep] = useState<
"upload" | "parsing" | "configure" | "preview"
>(id ? "configure" : "upload");
const [isUploading, setIsUploading] = useState(false);
const [parsedInfo, setParsedInfo] = useState({});
const [parseError, setParseError] = useState<string | null>(null);
const { handleUpload, createTask, taskList } = useFileSliceUpload(
{
preUpload: preUploadOperatorUsingPost,
uploadChunk: uploadOperatorChunkUsingPost,
cancelUpload: null,
},
false
);
// 模拟文件上传
const handleFileUpload = async (files: FileList) => {
setIsUploading(true);
setParseError(null);
try {
const fileName = files[0].name;
await handleUpload({
task: createTask({
dataset: { id: "operator-upload", name: "上传算子" },
}),
files: [
{
originFile: files[0],
slices: sliceFile(files[0]),
name: fileName,
size: files[0].size,
},
], // 假设只上传一个文件
});
setParsedInfo({ ...parsedInfo, percent: 100 }); // 上传完成,进度100%
// 解析文件过程
const res = await uploadOperatorUsingPost({ fileName });
const configs = res.data.settings && typeof res.data.settings === "string"
? JSON.parse(res.data.settings)
: {};
const defaultParams: Record<string, string> = {};
Object.keys(configs).forEach((key) => {
const { value } = configs[key];
defaultParams[key] = value;
});
setParsedInfo({ ...res.data, fileName, configs, defaultParams});
setUploadStep("parsing");
} catch (err) {
setParseError("文件解析失败," + err.data.message);
} finally {
setIsUploading(false);
setUploadStep("configure");
}
};
const handlePublish = async () => {
try {
if (id) {
await updateOperatorByIdUsingPut(id, parsedInfo!);
} else {
await createOperatorUsingPost(parsedInfo);
}
setUploadStep("preview");
} catch (err) {
message.error("算子发布失败," + err.data.message);
}
};
const onFetchOperator = async (operatorId: string) => {
// 编辑模式,加载已有算子信息逻辑待实现
const { data } = await queryOperatorByIdUsingGet(operatorId);
const configs = data.settings && typeof data.settings === "string"
? JSON.parse(data.settings)
: {};
const defaultParams: Record<string, string> = {};
Object.keys(configs).forEach((key) => {
const { value } = configs[key];
defaultParams[key] = value;
});
setParsedInfo({ ...data, configs, defaultParams});
setUploadStep("configure");
};
useEffect(() => {
if (id) {
// 编辑模式,加载已有算子信息逻辑待实现
onFetchOperator(id);
}
}, [id]);
return (
<div className="flex-overflow-auto bg-gray-50">
{/* Header */}
<div className="flex justify-between items-center">
<div className="flex items-center gap-4">
<Button type="text" onClick={() => navigate("/data/operator-market")}>
<ArrowLeft className="w-4 h-4" />
</Button>
<h1 className="text-xl font-bold text-gray-900">
{id ? "更新算子" : "上传算子"}
</h1>
</div>
<div className="w-1/2">
<Steps
size="small"
items={[
{
title: "上传文件",
icon: <Upload />,
},
{
title: "解析文件",
icon: <Settings />,
},
{
title: "配置信息",
icon: <TagIcon />,
},
{
title: "发布完成",
icon: <CheckCircle />,
},
]}
current={
uploadStep === "upload"
? 0
: uploadStep === "parsing"
? 1
: uploadStep === "configure"
? 2
: 3
}
/>
</div>
</div>
{/* Content */}
<div className="flex-overflow-auto p-6 mt-4 bg-white border-card">
<div className="flex-overflow-auto">
{uploadStep === "upload" && (
<UploadStep onUpload={handleFileUpload} isUploading={isUploading} />
)}
{uploadStep === "parsing" && (
<ParsingStep
parseProgress={taskList[0]?.percent || parsedInfo.percent || 0}
uploadedFiles={taskList}
/>
)}
{uploadStep === "configure" && (
<ConfigureStep
setParsedInfo={setParsedInfo}
parseError={parseError}
parsedInfo={parsedInfo}
/>
)}
{uploadStep === "preview" && (
<PreviewStep setUploadStep={setUploadStep} />
)}
</div>
{uploadStep === "configure" && (
<div className="flex justify-end gap-3 mt-8">
<Button onClick={() => setUploadStep("upload")}></Button>
<Button type="primary" onClick={handlePublish}>
{id ? "更新" : "发布"}
</Button>
</div>
)}
</div>
</div>
);
}
import { Button, App, Steps } from "antd";
import {
ArrowLeft,
CheckCircle,
Settings,
TagIcon,
Upload,
} from "lucide-react";
import { useNavigate, useParams } from "react-router";
import { useEffect, useState } from "react";
import UploadStep from "./components/UploadStep";
import ParsingStep from "./components/ParsingStep";
import ConfigureStep from "./components/ConfigureStep";
import PreviewStep from "./components/PreviewStep";
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
import {
createOperatorUsingPost,
preUploadOperatorUsingPost,
queryOperatorByIdUsingGet,
updateOperatorByIdUsingPut,
uploadOperatorChunkUsingPost,
uploadOperatorUsingPost,
} from "../operator.api";
import { sliceFile } from "@/utils/file.util";
export default function OperatorPluginCreate() {
const navigate = useNavigate();
const { id } = useParams();
const { message } = App.useApp();
const [uploadStep, setUploadStep] = useState<
"upload" | "parsing" | "configure" | "preview"
>(id ? "configure" : "upload");
const [isUploading, setIsUploading] = useState(false);
const [parsedInfo, setParsedInfo] = useState({});
const [parseError, setParseError] = useState<string | null>(null);
const { handleUpload, createTask, taskList } = useFileSliceUpload(
{
preUpload: preUploadOperatorUsingPost,
uploadChunk: uploadOperatorChunkUsingPost,
cancelUpload: null,
},
false
);
// 模拟文件上传
const handleFileUpload = async (files: FileList) => {
setIsUploading(true);
setParseError(null);
try {
const fileName = files[0].name;
await handleUpload({
task: createTask({
dataset: { id: "operator-upload", name: "上传算子" },
}),
files: [
{
originFile: files[0],
slices: sliceFile(files[0]),
name: fileName,
size: files[0].size,
},
], // 假设只上传一个文件
});
setParsedInfo({ ...parsedInfo, percent: 100 }); // 上传完成,进度100%
// 解析文件过程
const res = await uploadOperatorUsingPost({ fileName });
const configs = res.data.settings && typeof res.data.settings === "string"
? JSON.parse(res.data.settings)
: {};
const defaultParams: Record<string, string> = {};
Object.keys(configs).forEach((key) => {
const { value } = configs[key];
defaultParams[key] = value;
});
setParsedInfo({ ...res.data, fileName, configs, defaultParams});
setUploadStep("parsing");
} catch (err) {
setParseError("文件解析失败," + err.data.message);
} finally {
setIsUploading(false);
setUploadStep("configure");
}
};
const handlePublish = async () => {
try {
if (id) {
await updateOperatorByIdUsingPut(id, parsedInfo!);
} else {
await createOperatorUsingPost(parsedInfo);
}
setUploadStep("preview");
} catch (err) {
message.error("算子发布失败," + err.data.message);
}
};
const onFetchOperator = async (operatorId: string) => {
// 编辑模式,加载已有算子信息逻辑待实现
const { data } = await queryOperatorByIdUsingGet(operatorId);
const configs = data.settings && typeof data.settings === "string"
? JSON.parse(data.settings)
: {};
const defaultParams: Record<string, string> = {};
Object.keys(configs).forEach((key) => {
const { value } = configs[key];
defaultParams[key] = value;
});
setParsedInfo({ ...data, configs, defaultParams});
setUploadStep("configure");
};
useEffect(() => {
if (id) {
// 编辑模式,加载已有算子信息逻辑待实现
onFetchOperator(id);
}
}, [id]);
return (
<div className="flex-overflow-auto bg-gray-50">
{/* Header */}
<div className="flex justify-between items-center">
<div className="flex items-center gap-4">
<Button type="text" onClick={() => navigate("/data/operator-market")}>
<ArrowLeft className="w-4 h-4" />
</Button>
<h1 className="text-xl font-bold text-gray-900">
{id ? "更新算子" : "上传算子"}
</h1>
</div>
<div className="w-1/2">
<Steps
size="small"
items={[
{
title: "上传文件",
icon: <Upload />,
},
{
title: "解析文件",
icon: <Settings />,
},
{
title: "配置信息",
icon: <TagIcon />,
},
{
title: "发布完成",
icon: <CheckCircle />,
},
]}
current={
uploadStep === "upload"
? 0
: uploadStep === "parsing"
? 1
: uploadStep === "configure"
? 2
: 3
}
/>
</div>
</div>
{/* Content */}
<div className="flex-overflow-auto p-6 mt-4 bg-white border-card">
<div className="flex-overflow-auto">
{uploadStep === "upload" && (
<UploadStep onUpload={handleFileUpload} isUploading={isUploading} />
)}
{uploadStep === "parsing" && (
<ParsingStep
parseProgress={taskList[0]?.percent || parsedInfo.percent || 0}
uploadedFiles={taskList}
/>
)}
{uploadStep === "configure" && (
<ConfigureStep
setParsedInfo={setParsedInfo}
parseError={parseError}
parsedInfo={parsedInfo}
/>
)}
{uploadStep === "preview" && (
<PreviewStep setUploadStep={setUploadStep} />
)}
</div>
{uploadStep === "configure" && (
<div className="flex justify-end gap-3 mt-8">
<Button onClick={() => setUploadStep("upload")}></Button>
<Button type="primary" onClick={handlePublish}>
{id ? "更新" : "发布"}
</Button>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,115 +1,115 @@
import { Alert, Input, Form } from "antd";
import TextArea from "antd/es/input/TextArea";
import React, { useEffect } from "react";
import ParamConfig from "@/pages/DataCleansing/Create/components/ParamConfig.tsx";
export default function ConfigureStep({
parsedInfo,
parseError,
setParsedInfo,
}) {
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue(parsedInfo);
}, [parsedInfo]);
const handleConfigChange = (
operatorId: string,
paramKey: string,
value: any
) => {
setParsedInfo((op) =>
op.id === operatorId
? {
...op,
overrides: {
...(op?.overrides || op?.defaultParams),
[paramKey]: value,
},
}
: op
);
};
return (
<>
{/* 解析结果 */}
{parseError && (
<div className="mb-4">
<Alert
message="解析过程中发现问题"
description={parseError}
type="error"
showIcon
/>
</div>
)}
{!parseError && parsedInfo && (
<Form
form={form}
layout="vertical"
initialValues={parsedInfo}
onValuesChange={(_, allValues) => {
setParsedInfo({ ...parsedInfo, ...allValues });
}}
>
{/* 基本信息 */}
<h3 className="text-lg font-semibold text-gray-900"></h3>
<Form.Item label="ID" name="id" rules={[{ required: true }]}>
<Input value={parsedInfo.id} readOnly />
</Form.Item>
<Form.Item label="名称" name="name" rules={[{ required: true }]}>
<Input value={parsedInfo.name} />
</Form.Item>
<Form.Item label="版本" name="version" rules={[{ required: true }]}>
<Input value={parsedInfo.version} />
</Form.Item>
<Form.Item
label="描述"
name="description"
rules={[{ required: false }]}
>
<TextArea value={parsedInfo.description} />
</Form.Item>
<Form.Item
label="输入类型"
name="inputs"
rules={[{ required: true }]}
>
<Input value={parsedInfo.inputs} />
</Form.Item>
<Form.Item
label="输出类型"
name="outputs"
rules={[{ required: true }]}
>
<Input value={parsedInfo.outputs} />
</Form.Item>
{parsedInfo.configs && Object.keys(parsedInfo.configs).length > 0 && (
<>
<h3 className="text-lg font-semibold text-gray-900 mt-10 mb-2">
</h3>
<Form layout="vertical">
{Object.entries(parsedInfo?.configs).map(([key, param]) => (
<ParamConfig
key={key}
operator={parsedInfo}
paramKey={key}
param={param}
onParamChange={handleConfigChange}
/>
))}
</Form>
</>
)}
{/* <h3 className="text-lg font-semibold text-gray-900 mt-8">高级配置</h3> */}
</Form>
)}
</>
);
}
import { Alert, Input, Form } from "antd";
import TextArea from "antd/es/input/TextArea";
import React, { useEffect } from "react";
import ParamConfig from "@/pages/DataCleansing/Create/components/ParamConfig.tsx";
export default function ConfigureStep({
parsedInfo,
parseError,
setParsedInfo,
}) {
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue(parsedInfo);
}, [parsedInfo]);
const handleConfigChange = (
operatorId: string,
paramKey: string,
value: any
) => {
setParsedInfo((op) =>
op.id === operatorId
? {
...op,
overrides: {
...(op?.overrides || op?.defaultParams),
[paramKey]: value,
},
}
: op
);
};
return (
<>
{/* 解析结果 */}
{parseError && (
<div className="mb-4">
<Alert
message="解析过程中发现问题"
description={parseError}
type="error"
showIcon
/>
</div>
)}
{!parseError && parsedInfo && (
<Form
form={form}
layout="vertical"
initialValues={parsedInfo}
onValuesChange={(_, allValues) => {
setParsedInfo({ ...parsedInfo, ...allValues });
}}
>
{/* 基本信息 */}
<h3 className="text-lg font-semibold text-gray-900"></h3>
<Form.Item label="ID" name="id" rules={[{ required: true }]}>
<Input value={parsedInfo.id} readOnly />
</Form.Item>
<Form.Item label="名称" name="name" rules={[{ required: true }]}>
<Input value={parsedInfo.name} />
</Form.Item>
<Form.Item label="版本" name="version" rules={[{ required: true }]}>
<Input value={parsedInfo.version} />
</Form.Item>
<Form.Item
label="描述"
name="description"
rules={[{ required: false }]}
>
<TextArea value={parsedInfo.description} />
</Form.Item>
<Form.Item
label="输入类型"
name="inputs"
rules={[{ required: true }]}
>
<Input value={parsedInfo.inputs} />
</Form.Item>
<Form.Item
label="输出类型"
name="outputs"
rules={[{ required: true }]}
>
<Input value={parsedInfo.outputs} />
</Form.Item>
{parsedInfo.configs && Object.keys(parsedInfo.configs).length > 0 && (
<>
<h3 className="text-lg font-semibold text-gray-900 mt-10 mb-2">
</h3>
<Form layout="vertical">
{Object.entries(parsedInfo?.configs).map(([key, param]) => (
<ParamConfig
key={key}
operator={parsedInfo}
paramKey={key}
param={param}
onParamChange={handleConfigChange}
/>
))}
</Form>
</>
)}
{/* <h3 className="text-lg font-semibold text-gray-900 mt-8">高级配置</h3> */}
</Form>
)}
</>
);
}

View File

@@ -1,50 +1,50 @@
import { Progress } from "antd";
import { Settings, FileText, CheckCircle } from "lucide-react";
export default function ParsingStep({ parseProgress, uploadedFiles }) {
return (
<div className="text-center py-2">
<div className="w-24 h-24 mx-auto mb-6 bg-blue-50 rounded-full flex items-center justify-center">
<Settings className="w-12 h-12 text-blue-500 animate-spin" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">
</h2>
<p className="text-gray-600 mb-8">
...
</p>
{/* 已上传文件列表 */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="space-y-2">
{uploadedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-gray-500" />
<span className="font-medium">{file.name}</span>
<span className="text-sm text-gray-500">
({(file.size / 1024).toFixed(1)} KB)
</span>
</div>
<CheckCircle className="w-5 h-5 text-green-500" />
</div>
))}
</div>
</div>
{/* 解析进度 */}
<div className="max-w-md mx-auto">
<Progress
percent={parseProgress}
status="active"
strokeColor="#3B82F6"
/>
<p className="mt-2 text-sm text-gray-600">: {parseProgress}%</p>
</div>
</div>
);
}
import { Progress } from "antd";
import { Settings, FileText, CheckCircle } from "lucide-react";
export default function ParsingStep({ parseProgress, uploadedFiles }) {
return (
<div className="text-center py-2">
<div className="w-24 h-24 mx-auto mb-6 bg-blue-50 rounded-full flex items-center justify-center">
<Settings className="w-12 h-12 text-blue-500 animate-spin" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">
</h2>
<p className="text-gray-600 mb-8">
...
</p>
{/* 已上传文件列表 */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="space-y-2">
{uploadedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-gray-500" />
<span className="font-medium">{file.name}</span>
<span className="text-sm text-gray-500">
({(file.size / 1024).toFixed(1)} KB)
</span>
</div>
<CheckCircle className="w-5 h-5 text-green-500" />
</div>
))}
</div>
</div>
{/* 解析进度 */}
<div className="max-w-md mx-auto">
<Progress
percent={parseProgress}
status="active"
strokeColor="#3B82F6"
/>
<p className="mt-2 text-sm text-gray-600">: {parseProgress}%</p>
</div>
</div>
);
}

View File

@@ -1,26 +1,26 @@
import { Button } from "antd";
import { CheckCircle, Plus } from "lucide-react";
import { useNavigate } from "react-router";
export default function PreviewStep({ setUploadStep }) {
const navigate = useNavigate();
return (
<div className="text-center py-2">
<div className="w-24 h-24 mx-auto mb-6 bg-green-50 rounded-full flex items-center justify-center">
<CheckCircle className="w-12 h-12 text-green-500" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4"></h2>
<p className="text-gray-600 mb-8"></p>
<div className="flex justify-center gap-4">
<Button onClick={() => setUploadStep("upload")}>
<Plus className="w-4 h-4 mr-2" />
</Button>
<Button type="primary" onClick={() => navigate("/data/operator-market")}>
</Button>
</div>
</div>
);
}
import { Button } from "antd";
import { CheckCircle, Plus } from "lucide-react";
import { useNavigate } from "react-router";
export default function PreviewStep({ setUploadStep }) {
const navigate = useNavigate();
return (
<div className="text-center py-2">
<div className="w-24 h-24 mx-auto mb-6 bg-green-50 rounded-full flex items-center justify-center">
<CheckCircle className="w-12 h-12 text-green-500" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4"></h2>
<p className="text-gray-600 mb-8"></p>
<div className="flex justify-center gap-4">
<Button onClick={() => setUploadStep("upload")}>
<Plus className="w-4 h-4 mr-2" />
</Button>
<Button type="primary" onClick={() => navigate("/data/operator-market")}>
</Button>
</div>
</div>
);
}

View File

@@ -1,79 +1,79 @@
import { Spin } from "antd";
import { Upload, FileText } from "lucide-react";
export default function UploadStep({ isUploading, onUpload }) {
const supportedFormats = [
{ ext: ".zip", desc: "压缩包文件" },
{ ext: ".tar", desc: "压缩包文件" },
];
return (
<div className="py-2 w-full text-center">
<div className="w-24 h-24 mx-auto mb-6 bg-blue-50 rounded-full flex items-center justify-center">
<Upload className="w-12 h-12 text-blue-500" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4"></h2>
<p className="text-gray-600 mb-8">
</p>
{/* 支持的格式 */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
</h3>
<div className="flex gap-4">
{supportedFormats.map((format, index) => (
<div key={index} className="p-3 border border-gray-200 rounded-lg flex-1">
<div className="font-medium text-gray-900">{format.ext}</div>
<div className="text-sm text-gray-500">{format.desc}</div>
</div>
))}
</div>
</div>
{/* 文件上传区域 */}
<div
className="border-2 border-dashed border-gray-300 rounded-lg p-8 hover:border-blue-400 transition-colors cursor-pointer"
onDrop={(e) => {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length > 0) {
onUpload(files);
}
}}
onDragOver={(e) => e.preventDefault()}
onClick={() => {
const input = document.createElement("input");
input.type = "file";
input.multiple = false;
input.accept = supportedFormats.map((f) => f.ext).join(",");
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files;
if (files) {
onUpload(files);
}
};
input.click();
}}
>
{isUploading ? (
<div className="flex flex-col items-center">
<Spin size="large" />
<p className="mt-4 text-gray-600">...</p>
</div>
) : (
<div>
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-lg text-gray-600 mb-2">
</p>
<p className="text-sm text-gray-500">
</p>
</div>
)}
</div>
</div>
);
}
import { Spin } from "antd";
import { Upload, FileText } from "lucide-react";
export default function UploadStep({ isUploading, onUpload }) {
const supportedFormats = [
{ ext: ".zip", desc: "压缩包文件" },
{ ext: ".tar", desc: "压缩包文件" },
];
return (
<div className="py-2 w-full text-center">
<div className="w-24 h-24 mx-auto mb-6 bg-blue-50 rounded-full flex items-center justify-center">
<Upload className="w-12 h-12 text-blue-500" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4"></h2>
<p className="text-gray-600 mb-8">
</p>
{/* 支持的格式 */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
</h3>
<div className="flex gap-4">
{supportedFormats.map((format, index) => (
<div key={index} className="p-3 border border-gray-200 rounded-lg flex-1">
<div className="font-medium text-gray-900">{format.ext}</div>
<div className="text-sm text-gray-500">{format.desc}</div>
</div>
))}
</div>
</div>
{/* 文件上传区域 */}
<div
className="border-2 border-dashed border-gray-300 rounded-lg p-8 hover:border-blue-400 transition-colors cursor-pointer"
onDrop={(e) => {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length > 0) {
onUpload(files);
}
}}
onDragOver={(e) => e.preventDefault()}
onClick={() => {
const input = document.createElement("input");
input.type = "file";
input.multiple = false;
input.accept = supportedFormats.map((f) => f.ext).join(",");
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files;
if (files) {
onUpload(files);
}
};
input.click();
}}
>
{isUploading ? (
<div className="flex flex-col items-center">
<Spin size="large" />
<p className="mt-4 text-gray-600">...</p>
</div>
) : (
<div>
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-lg text-gray-600 mb-2">
</p>
<p className="text-sm text-gray-500">
</p>
</div>
)}
</div>
</div>
);
}