Implement ComponentsModelsPage with model management features
Introduced the ComponentsModelsPage to manage various model types (LLM, ASR, TTS, Realtime, Embedding) with a user-friendly interface. Added functionality for creating, editing, and filtering models, along with a search feature. Enhanced the layout with new UI components, including buttons, dropdowns, and dialogs for improved user interaction and experience.
This commit is contained in:
@@ -1,11 +1,612 @@
|
||||
import { PlaceholderPage } from "./PlaceholderPage";
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Brain,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
EyeOff,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
Plus,
|
||||
Search,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useState } from "react";
|
||||
|
||||
type ModelType = "LLM" | "ASR" | "TTS" | "Realtime" | "Embedding";
|
||||
|
||||
type InterfaceType = "openai" | "xfyun" | "dashscope" | "gemini";
|
||||
|
||||
/** 各资源类型可选的接口类型 */
|
||||
const interfaceOptionsByType: Record<ModelType, InterfaceType[]> = {
|
||||
LLM: ["openai"],
|
||||
ASR: ["openai", "xfyun", "dashscope"],
|
||||
TTS: ["openai", "xfyun", "dashscope"],
|
||||
Realtime: ["openai", "gemini"],
|
||||
Embedding: ["openai"],
|
||||
};
|
||||
|
||||
const modelTypes: ModelType[] = ["LLM", "ASR", "TTS", "Realtime", "Embedding"];
|
||||
|
||||
type ModelResource = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ModelType;
|
||||
interfaceType: InterfaceType;
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
const mockModels: ModelResource[] = [
|
||||
{
|
||||
id: "model_001",
|
||||
name: "DeepSeek-V3",
|
||||
type: "LLM",
|
||||
interfaceType: "openai",
|
||||
apiUrl: "https://api.deepseek.com/v1",
|
||||
apiKey: "sk-deepseek-7f3a9c2e1b",
|
||||
},
|
||||
{
|
||||
id: "model_002",
|
||||
name: "Qwen-Max",
|
||||
type: "LLM",
|
||||
interfaceType: "openai",
|
||||
apiUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
apiKey: "sk-qwen-4d8e2a6f0c",
|
||||
},
|
||||
{
|
||||
id: "model_003",
|
||||
name: "讯飞语音识别",
|
||||
type: "ASR",
|
||||
interfaceType: "xfyun",
|
||||
apiUrl: "https://iat-api.xfyun.cn/v2/iat",
|
||||
apiKey: "xf-asr-9b1c3d5e7a",
|
||||
},
|
||||
{
|
||||
id: "model_004",
|
||||
name: "Paraformer 识别",
|
||||
type: "ASR",
|
||||
interfaceType: "dashscope",
|
||||
apiUrl: "https://dashscope.aliyuncs.com/api/v1/services/audio/asr",
|
||||
apiKey: "sk-paraformer-2e4f6a",
|
||||
},
|
||||
{
|
||||
id: "model_005",
|
||||
name: "讯飞语音合成",
|
||||
type: "TTS",
|
||||
interfaceType: "xfyun",
|
||||
apiUrl: "https://tts-api.xfyun.cn/v2/tts",
|
||||
apiKey: "xf-tts-6c8a0b2d4f",
|
||||
},
|
||||
{
|
||||
id: "model_006",
|
||||
name: "CosyVoice 合成",
|
||||
type: "TTS",
|
||||
interfaceType: "dashscope",
|
||||
apiUrl: "https://dashscope.aliyuncs.com/api/v1/services/audio/tts",
|
||||
apiKey: "sk-cosyvoice-1a3c5e",
|
||||
},
|
||||
{
|
||||
id: "model_007",
|
||||
name: "OpenAI TTS",
|
||||
type: "TTS",
|
||||
interfaceType: "openai",
|
||||
apiUrl: "https://api.openai.com/v1/audio/speech",
|
||||
apiKey: "sk-openai-tts-8f0a2c",
|
||||
},
|
||||
{
|
||||
id: "model_008",
|
||||
name: "GPT Realtime",
|
||||
type: "Realtime",
|
||||
interfaceType: "openai",
|
||||
apiUrl: "https://api.openai.com/v1/realtime",
|
||||
apiKey: "sk-realtime-3b5d7f9a1c",
|
||||
},
|
||||
{
|
||||
id: "model_009",
|
||||
name: "Gemini Live",
|
||||
type: "Realtime",
|
||||
interfaceType: "gemini",
|
||||
apiUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
apiKey: "gm-live-5e7a9c1b3d",
|
||||
},
|
||||
{
|
||||
id: "model_010",
|
||||
name: "text-embedding-3",
|
||||
type: "Embedding",
|
||||
interfaceType: "openai",
|
||||
apiUrl: "https://api.openai.com/v1/embeddings",
|
||||
apiKey: "sk-embed-0c2e4a6f8b",
|
||||
},
|
||||
{
|
||||
id: "model_011",
|
||||
name: "Kimi-K2",
|
||||
type: "LLM",
|
||||
interfaceType: "openai",
|
||||
apiUrl: "https://api.moonshot.cn/v1",
|
||||
apiKey: "sk-kimi-7a9c1e3b5d",
|
||||
},
|
||||
{
|
||||
id: "model_012",
|
||||
name: "BGE Embedding",
|
||||
type: "Embedding",
|
||||
interfaceType: "openai",
|
||||
apiUrl: "https://api.siliconflow.cn/v1/embeddings",
|
||||
apiKey: "sk-bge-2d4f6a8c0e",
|
||||
},
|
||||
];
|
||||
|
||||
type ModelForm = {
|
||||
name: string;
|
||||
type: ModelType;
|
||||
interfaceType: InterfaceType;
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
const emptyForm: ModelForm = {
|
||||
name: "",
|
||||
type: "LLM",
|
||||
interfaceType: "openai",
|
||||
apiUrl: "",
|
||||
apiKey: "",
|
||||
};
|
||||
|
||||
type TypeFilter = "全部" | ModelType;
|
||||
|
||||
const typeFilters: TypeFilter[] = ["全部", ...modelTypes];
|
||||
|
||||
export function ComponentsModelsPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [typeFilter, setTypeFilter] = useState<TypeFilter>("全部");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [form, setForm] = useState<ModelForm>(emptyForm);
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
|
||||
function updateForm<K extends keyof ModelForm>(key: K, value: ModelForm[K]) {
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
}
|
||||
|
||||
function handleTypeChange(type: ModelType) {
|
||||
const options = interfaceOptionsByType[type];
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
type,
|
||||
// 资源类型变化时,若当前接口类型不在新类型的可选项内则重置为首项
|
||||
interfaceType: options.includes(prev.interfaceType)
|
||||
? prev.interfaceType
|
||||
: options[0],
|
||||
}));
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
setEditingId(null);
|
||||
setForm(emptyForm);
|
||||
setShowKey(false);
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function openEdit(model: ModelResource) {
|
||||
setEditingId(model.id);
|
||||
setForm({
|
||||
name: model.name,
|
||||
type: model.type,
|
||||
interfaceType: model.interfaceType,
|
||||
apiUrl: model.apiUrl,
|
||||
apiKey: model.apiKey,
|
||||
});
|
||||
setShowKey(false);
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
const filteredModels = mockModels.filter((model) => {
|
||||
if (typeFilter !== "全部" && model.type !== typeFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keyword = searchQuery.trim().toLowerCase();
|
||||
if (!keyword) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [model.name, model.id, model.type, model.interfaceType, model.apiUrl]
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
.includes(keyword);
|
||||
});
|
||||
|
||||
const pageSize = 5;
|
||||
const totalPages = Math.max(1, Math.ceil(filteredModels.length / pageSize));
|
||||
const safeCurrentPage = Math.min(currentPage, totalPages);
|
||||
const pageStart = (safeCurrentPage - 1) * pageSize;
|
||||
const pageEnd = pageStart + pageSize;
|
||||
const paginatedModels = filteredModels.slice(pageStart, pageEnd);
|
||||
|
||||
function handleSearchChange(value: string) {
|
||||
setSearchQuery(value);
|
||||
setCurrentPage(1);
|
||||
}
|
||||
|
||||
function handleFilterChange(filter: TypeFilter) {
|
||||
setTypeFilter(filter);
|
||||
setCurrentPage(1);
|
||||
}
|
||||
|
||||
const interfaceOptions = interfaceOptionsByType[form.type];
|
||||
const canSave = form.name.trim() && form.apiUrl.trim();
|
||||
|
||||
return (
|
||||
<PlaceholderPage
|
||||
label="资源管理"
|
||||
title="模型资源"
|
||||
description="统一管理大语言模型、语音识别、声音资源、知识库与工具插件。"
|
||||
/>
|
||||
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-8">
|
||||
<div className="flex flex-col items-start justify-between gap-5 sm:flex-row sm:gap-6">
|
||||
<div>
|
||||
<div className="caption-label text-muted-soft">资源管理</div>
|
||||
<h1 className="font-display display-lg mt-3 text-ink">模型资源</h1>
|
||||
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-muted-foreground">
|
||||
统一管理 LLM、ASR、TTS、Realtime 与 Embedding 模型,配置各自的接口类型与访问凭证。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full shrink-0 gap-2 sm:w-auto"
|
||||
onClick={openCreate}
|
||||
>
|
||||
<Plus size={16} />
|
||||
添加模型
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
|
||||
<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div className="text-[18px] font-medium text-foreground">
|
||||
模型列表
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">
|
||||
共 {mockModels.length} 个模型
|
||||
{(searchQuery.trim() || typeFilter !== "全部") &&
|
||||
`,已筛选 ${filteredModels.length} 个`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full md:w-[320px]">
|
||||
<Search
|
||||
size={15}
|
||||
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-soft"
|
||||
/>
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(event) => handleSearchChange(event.target.value)}
|
||||
className="h-10 border-hairline-strong bg-background pl-9 text-sm text-foreground placeholder:text-muted-soft"
|
||||
placeholder="搜索模型名称、接口类型或 URL..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-5 flex flex-wrap gap-2">
|
||||
{typeFilters.map((filter) => (
|
||||
<Button
|
||||
key={filter}
|
||||
variant={filter === typeFilter ? "default" : "outline"}
|
||||
size="sm"
|
||||
className={
|
||||
filter === typeFilter
|
||||
? "rounded-full"
|
||||
: "rounded-full border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||
}
|
||||
onClick={() => handleFilterChange(filter)}
|
||||
>
|
||||
{filter}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden rounded-xl border border-hairline">
|
||||
<div className="hidden items-center gap-4 bg-surface-strong/60 px-5 py-3 md:flex">
|
||||
<div className="caption-label flex-1 text-muted-soft">模型名称</div>
|
||||
<div className="caption-label w-[110px] text-muted-soft">
|
||||
资源类型
|
||||
</div>
|
||||
<div className="caption-label w-[110px] text-muted-soft">
|
||||
接口类型
|
||||
</div>
|
||||
<div className="caption-label w-[260px] text-muted-soft">
|
||||
API URL
|
||||
</div>
|
||||
<div className="caption-label w-[116px] text-right text-muted-soft">
|
||||
操作
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-hairline">
|
||||
{paginatedModels.map((model) => (
|
||||
<div
|
||||
key={model.id}
|
||||
className="flex flex-col gap-3 px-5 py-4 text-sm transition-colors hover:bg-surface-strong/40 md:flex-row md:items-center md:gap-4"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium text-foreground">
|
||||
{model.name}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-soft">{model.id}</div>
|
||||
</div>
|
||||
|
||||
<div className="md:w-[110px]">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-6 bg-surface-strong px-3 text-muted-foreground"
|
||||
>
|
||||
{model.type}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="md:w-[110px]">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-6 bg-surface-strong px-3 text-muted-foreground"
|
||||
>
|
||||
{model.interfaceType}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="truncate text-muted-foreground md:w-[260px]">
|
||||
{model.apiUrl}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 md:w-[116px]">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1.5 border-hairline-strong text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={() => openEdit(model)}
|
||||
>
|
||||
<Pencil size={14} />
|
||||
编辑
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||
aria-label={`${model.name} 更多操作`}
|
||||
>
|
||||
<MoreHorizontal size={15} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-32 min-w-32 rounded-xl border border-hairline bg-popover p-1"
|
||||
>
|
||||
<DropdownMenuItem variant="destructive" className="rounded-lg">
|
||||
<Trash2 size={14} />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredModels.length === 0 && (
|
||||
<div className="px-5 py-12 text-center">
|
||||
<div className="font-medium text-foreground">
|
||||
未找到匹配的模型
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-muted-foreground">
|
||||
请调整关键词或筛选条件后再试。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-col gap-3 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
{filteredModels.length === 0
|
||||
? "没有数据"
|
||||
: `显示 ${pageStart + 1}-${Math.min(pageEnd, filteredModels.length)} / 共 ${filteredModels.length} 个模型`}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||
disabled={safeCurrentPage <= 1}
|
||||
onClick={() => setCurrentPage((page) => Math.max(1, page - 1))}
|
||||
aria-label="上一页"
|
||||
>
|
||||
<ChevronLeft size={15} />
|
||||
</Button>
|
||||
|
||||
{Array.from({ length: totalPages }, (_, index) => index + 1).map(
|
||||
(page) => (
|
||||
<Button
|
||||
key={page}
|
||||
variant={page === safeCurrentPage ? "default" : "outline"}
|
||||
size="sm"
|
||||
className={[
|
||||
"h-8 min-w-8 px-2",
|
||||
page === safeCurrentPage
|
||||
? ""
|
||||
: "border-hairline-strong text-muted-foreground hover:text-foreground",
|
||||
].join(" ")}
|
||||
onClick={() => setCurrentPage(page)}
|
||||
>
|
||||
{page}
|
||||
</Button>
|
||||
),
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||
disabled={safeCurrentPage >= totalPages}
|
||||
onClick={() =>
|
||||
setCurrentPage((page) => Math.min(totalPages, page + 1))
|
||||
}
|
||||
aria-label="下一页"
|
||||
>
|
||||
<ChevronRight size={15} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingId ? "编辑模型" : "添加模型"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
填写模型名称、资源类型、接口类型与访问凭证。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="block">
|
||||
<div className="mb-2 text-sm font-medium text-foreground">名称</div>
|
||||
<Input
|
||||
value={form.name}
|
||||
autoFocus
|
||||
onChange={(event) => updateForm("name", event.target.value)}
|
||||
placeholder="请输入模型名称"
|
||||
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="block">
|
||||
<div className="mb-2 text-sm font-medium text-foreground">
|
||||
资源类型
|
||||
</div>
|
||||
<Select
|
||||
value={form.type}
|
||||
onValueChange={(value) => handleTypeChange(value as ModelType)}
|
||||
>
|
||||
<SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
|
||||
<SelectValue placeholder="请选择资源类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="border-hairline bg-popover text-popover-foreground">
|
||||
{modelTypes.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="block">
|
||||
<div className="mb-2 text-sm font-medium text-foreground">
|
||||
接口类型
|
||||
</div>
|
||||
<Select
|
||||
value={form.interfaceType}
|
||||
onValueChange={(value) =>
|
||||
updateForm("interfaceType", value as InterfaceType)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
|
||||
<SelectValue placeholder="请选择接口类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="border-hairline bg-popover text-popover-foreground">
|
||||
{interfaceOptions.map((item) => (
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-2 text-sm font-medium text-foreground">
|
||||
API URL
|
||||
</div>
|
||||
<Input
|
||||
value={form.apiUrl}
|
||||
onChange={(event) => updateForm("apiUrl", event.target.value)}
|
||||
placeholder="https://api.example.com/v1"
|
||||
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-2 text-sm font-medium text-foreground">
|
||||
API Key
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={form.apiKey}
|
||||
type={showKey ? "text" : "password"}
|
||||
onChange={(event) => updateForm("apiKey", event.target.value)}
|
||||
placeholder="请输入 API Key"
|
||||
className="border-hairline-strong bg-background pr-10 text-foreground placeholder:text-muted-soft"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowKey((value) => !value)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-soft transition-colors hover:text-foreground"
|
||||
aria-label={showKey ? "隐藏 API Key" : "显示 API Key"}
|
||||
>
|
||||
{showKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={!canSave}
|
||||
onClick={() => setDialogOpen(false)}
|
||||
>
|
||||
<Brain size={16} />
|
||||
{editingId ? "保存" : "添加"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user