生产管理系统 模型服务接入、模型服务管理2个页面开发
This commit is contained in:
@@ -0,0 +1,217 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
Package,
|
||||||
|
Plus,
|
||||||
|
CheckCircle,
|
||||||
|
AlertCircle,
|
||||||
|
RefreshCw,
|
||||||
|
Trash2,
|
||||||
|
Clock,
|
||||||
|
Settings,
|
||||||
|
Download,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface ModelService {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
dependencies: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DependencyManageDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
model: ModelService | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DependencyManageDialog({ open, onOpenChange, model }: DependencyManageDialogProps) {
|
||||||
|
const handleUpdateDependency = () => {
|
||||||
|
toast.success('依赖已更新');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveDependency = (dep: string) => {
|
||||||
|
toast.success(`依赖 ${dep} 已移除`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddDependency = () => {
|
||||||
|
toast.success('新依赖已添加');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!model) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>依赖管理 - {model.name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
管理模型运行所需的依赖包和环境配置
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 当前依赖 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h4 className="flex items-center gap-2">
|
||||||
|
<Package className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
当前依赖 ({model.dependencies.length}个)
|
||||||
|
</h4>
|
||||||
|
<Button size="sm" variant="outline" onClick={handleAddDependency}>
|
||||||
|
<Plus className="w-3 h-3 mr-1" />
|
||||||
|
添加依赖
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{model.dependencies.map((dep, idx) => (
|
||||||
|
<div key={idx} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-900 rounded-lg group">
|
||||||
|
<div className="flex items-center gap-3 flex-1">
|
||||||
|
<Package className="w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0" />
|
||||||
|
<code className="font-mono text-sm flex-1">{dep}</code>
|
||||||
|
<Badge variant="outline" className="text-xs font-light">已安装</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<Button size="sm" variant="ghost" onClick={handleUpdateDependency} title="更新版本">
|
||||||
|
<RefreshCw className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost" onClick={() => handleRemoveDependency(dep)} title="移除">
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 依赖检查 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||||
|
依赖健康检查
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between p-3 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">所有依赖已安装</div>
|
||||||
|
<div className="text-xs text-muted-foreground">版本兼容性检查通过</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300">正常</Badge>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" className="w-full">
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
重新检查依赖
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 依赖冲突检测 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<AlertCircle className="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
|
||||||
|
冲突检测
|
||||||
|
</h4>
|
||||||
|
<div className="p-4 bg-yellow-50 dark:bg-yellow-950 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertCircle className="w-5 h-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-yellow-900 dark:text-yellow-100 mb-2">发现潜在版本冲突</div>
|
||||||
|
<div className="text-sm text-yellow-800 dark:text-yellow-200 space-y-1">
|
||||||
|
<div>• numpy==1.24.0 与 tensorflow==2.13.0 可能存在兼容性问题</div>
|
||||||
|
<div>• 建议升级 numpy 到 1.24.3 或更高版本</div>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" className="mt-3" variant="outline">
|
||||||
|
自动修复
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 环境配置 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Settings className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||||
|
环境配置
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Python版本</Label>
|
||||||
|
<Select defaultValue="3.9">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="3.8">Python 3.8</SelectItem>
|
||||||
|
<SelectItem value="3.9">Python 3.9</SelectItem>
|
||||||
|
<SelectItem value="3.10">Python 3.10</SelectItem>
|
||||||
|
<SelectItem value="3.11">Python 3.11</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>CUDA版本</Label>
|
||||||
|
<Select defaultValue="11.8">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">不使用CUDA</SelectItem>
|
||||||
|
<SelectItem value="11.7">CUDA 11.7</SelectItem>
|
||||||
|
<SelectItem value="11.8">CUDA 11.8</SelectItem>
|
||||||
|
<SelectItem value="12.0">CUDA 12.0</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 依赖更新日志 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Clock className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
||||||
|
更新历史
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2 max-h-[150px] overflow-y-auto">
|
||||||
|
{[
|
||||||
|
{ date: '2024-10-20', action: '更新 tensorflow 2.12.0 → 2.13.0', user: '张三' },
|
||||||
|
{ date: '2024-10-15', action: '添加 opencv-python==4.8.0', user: '李四' },
|
||||||
|
{ date: '2024-10-10', action: '更新 numpy 1.23.0 → 1.24.0', user: '王五' },
|
||||||
|
].map((log, idx) => (
|
||||||
|
<div key={idx} className="flex items-start gap-3 p-2 text-sm">
|
||||||
|
<Clock className="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs text-muted-foreground">{log.date}</div>
|
||||||
|
<div>{log.action}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">by {log.user}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600">
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
导出依赖清单
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
Server,
|
||||||
|
CheckCircle,
|
||||||
|
Eye,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface DeployConfigDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeployConfigDialog({ open, onOpenChange }: DeployConfigDialogProps) {
|
||||||
|
const handleDeploy = () => {
|
||||||
|
toast.success('模型部署已启动,预计3-5分钟完成');
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>模型部署配置</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
配置模型的部署环境和资源分配
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 部署环境 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4">部署环境</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>环境类型</Label>
|
||||||
|
<Select defaultValue="production">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="development">开发环境</SelectItem>
|
||||||
|
<SelectItem value="staging">测试环境</SelectItem>
|
||||||
|
<SelectItem value="production">生产环境</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>部署区域</Label>
|
||||||
|
<Select defaultValue="cn-east">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="cn-east">华东</SelectItem>
|
||||||
|
<SelectItem value="cn-north">华北</SelectItem>
|
||||||
|
<SelectItem value="cn-south">华南</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 资源配置 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4">资源配置</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>CPU配置</Label>
|
||||||
|
<Select defaultValue="2">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1">1核</SelectItem>
|
||||||
|
<SelectItem value="2">2核</SelectItem>
|
||||||
|
<SelectItem value="4">4核</SelectItem>
|
||||||
|
<SelectItem value="8">8核</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>内存配置</Label>
|
||||||
|
<Select defaultValue="4">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="2">2GB</SelectItem>
|
||||||
|
<SelectItem value="4">4GB</SelectItem>
|
||||||
|
<SelectItem value="8">8GB</SelectItem>
|
||||||
|
<SelectItem value="16">16GB</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>GPU配置</Label>
|
||||||
|
<Select defaultValue="none">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">不使用GPU</SelectItem>
|
||||||
|
<SelectItem value="t4">NVIDIA T4</SelectItem>
|
||||||
|
<SelectItem value="v100">NVIDIA V100</SelectItem>
|
||||||
|
<SelectItem value="a100">NVIDIA A100</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>副本数量</Label>
|
||||||
|
<Input type="number" defaultValue="3" min="1" max="10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 自动伸缩 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4">自动伸缩策略</h4>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between p-3 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">启用自动伸缩</div>
|
||||||
|
<div className="text-xs text-muted-foreground">根据负载自动调整实例数量</div>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>最小实例数</Label>
|
||||||
|
<Input type="number" defaultValue="2" min="1" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>最大实例数</Label>
|
||||||
|
<Input type="number" defaultValue="10" max="50" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>扩容阈值(CPU)</Label>
|
||||||
|
<Input type="number" defaultValue="70" min="0" max="100" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>缩容阈值(CPU)</Label>
|
||||||
|
<Input type="number" defaultValue="30" min="0" max="100" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 健康检查 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4">健康检查</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>检查间隔(秒)</Label>
|
||||||
|
<Input type="number" defaultValue="30" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>超时时间(秒)</Label>
|
||||||
|
<Input type="number" defaultValue="10" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>失败阈值</Label>
|
||||||
|
<Input type="number" defaultValue="3" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>成功阈值</Label>
|
||||||
|
<Input type="number" defaultValue="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 部署进度预估 */}
|
||||||
|
<Card className="p-4 bg-gradient-to-r from-green-50 to-teal-50 dark:from-green-950 dark:to-teal-950">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Server className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-green-900 dark:text-green-100 mb-2">部署流程</h4>
|
||||||
|
<div className="space-y-2 text-xs text-green-800 dark:text-green-200">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
<span>1. 模型文件准备与验证 (~1分钟)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
<span>2. 容器镜像构建 (~2分钟)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
<span>3. 服务实例启动 (~1分钟)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
<span>4. 健康检查与负载均衡配置 (~1分钟)</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-green-600 dark:text-green-400">预计总时间: 3-5分钟</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline">
|
||||||
|
<Eye className="w-4 h-4 mr-2" />
|
||||||
|
预览配置
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600" onClick={handleDeploy}>
|
||||||
|
<Server className="w-4 h-4 mr-2" />
|
||||||
|
开始部署
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
Brain,
|
||||||
|
BarChart3,
|
||||||
|
Link,
|
||||||
|
Package,
|
||||||
|
Terminal,
|
||||||
|
CheckCircle,
|
||||||
|
GitBranch,
|
||||||
|
Copy,
|
||||||
|
Eye,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface ModelService {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
type: string;
|
||||||
|
format: string;
|
||||||
|
description: string;
|
||||||
|
author: string;
|
||||||
|
createTime: string;
|
||||||
|
lastUpdateTime: string;
|
||||||
|
status: string;
|
||||||
|
endpoint: string;
|
||||||
|
accessLevel: string;
|
||||||
|
tags: string[];
|
||||||
|
accuracy?: number;
|
||||||
|
inferenceTime?: number;
|
||||||
|
requestCount: number;
|
||||||
|
successRate: number;
|
||||||
|
dependencies: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelDetailDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
model: ModelService | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModelDetailDialog({ open, onOpenChange, model }: ModelDetailDialogProps) {
|
||||||
|
const handleCopyEndpoint = async (endpoint: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(endpoint);
|
||||||
|
toast.success('端点已复制到剪贴板');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('复制失败,请重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestModel = () => {
|
||||||
|
toast.success('模型测试成功,推理正常');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAccessLevelIcon = (level: string) => {
|
||||||
|
switch (level) {
|
||||||
|
case '公开': return '🌐';
|
||||||
|
case '私有': return '🔒';
|
||||||
|
case '团队共享': return '👥';
|
||||||
|
default: return '🔒';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!model) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>模型详情 - {model.name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
查看模型完整信息、元数据和运行状态
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 基本信息 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Brain className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
基本信息
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 模型名称 - 大字体显示 */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-2xl font-bold text-foreground mb-2">{model.name}</h3>
|
||||||
|
<Badge variant="outline" className="text-sm">
|
||||||
|
<GitBranch className="w-3 h-3 mr-1" />
|
||||||
|
{model.version}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">模型类型</Label>
|
||||||
|
<p className="mt-1">
|
||||||
|
<Badge variant="outline" className="font-light">{model.type}</Badge>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">模型格式</Label>
|
||||||
|
<p className="mt-1">{model.format}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">模型描述</Label>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">{model.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">访问权限</Label>
|
||||||
|
<p className="mt-1 flex items-center gap-2">
|
||||||
|
<span>{getAccessLevelIcon(model.accessLevel)}</span>
|
||||||
|
<span className="text-sm">{model.accessLevel}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">标签</Label>
|
||||||
|
<div className="mt-1 flex flex-wrap gap-2">
|
||||||
|
{model.tags.map((tag, idx) => (
|
||||||
|
<Badge key={idx} variant="outline" className="text-xs font-light">{tag}</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 性能指标 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<BarChart3 className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||||
|
性能指标
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<div className="text-center p-4 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground">模型准确率</p>
|
||||||
|
<p className="text-2xl text-green-600 dark:text-green-400 mt-1">{model.accuracy}%</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground">推理时间</p>
|
||||||
|
<p className="text-2xl text-blue-600 dark:text-blue-400 mt-1">{model.inferenceTime}ms</p>
|
||||||
|
<p className="text-xs text-blue-600 dark:text-blue-400 mt-1">平均响应</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-purple-50 dark:bg-purple-950 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground">调用次数</p>
|
||||||
|
<p className="text-2xl text-purple-600 dark:text-purple-400 mt-1">{model.requestCount.toLocaleString()}</p>
|
||||||
|
<p className="text-xs text-purple-600 dark:text-purple-400 mt-1">总计</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-orange-50 dark:bg-orange-950 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground">成功率</p>
|
||||||
|
<p className="text-2xl text-orange-600 dark:text-orange-400 mt-1">{model.successRate}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* API端点信息 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Link className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
API端点
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">服务端点</Label>
|
||||||
|
<div className="mt-2 flex items-center gap-2">
|
||||||
|
<code className="flex-1 bg-gray-900 dark:bg-gray-950 text-green-400 px-4 py-2 rounded text-sm font-mono">
|
||||||
|
{model.endpoint}
|
||||||
|
</code>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => handleCopyEndpoint(model.endpoint)}>
|
||||||
|
复制
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">请求方式</Label>
|
||||||
|
<p className="mt-1 text-sm">POST</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">Content-Type</Label>
|
||||||
|
<p className="mt-1 text-sm">application/json</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 依赖包列表 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Package className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||||
|
依赖包 ({model.dependencies.length}个)
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{model.dependencies.map((dep, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-900 rounded text-sm">
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0" />
|
||||||
|
<code className="font-mono">{dep}</code>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 调用示例 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Terminal className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||||
|
API调用示例
|
||||||
|
</h4>
|
||||||
|
<div className="bg-gray-900 dark:bg-gray-950 text-green-400 p-4 rounded-lg font-mono text-sm overflow-x-auto">
|
||||||
|
<pre>{`# Python调用示例
|
||||||
|
import requests
|
||||||
|
|
||||||
|
url = "${model.endpoint}"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer YOUR_API_KEY"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"data": [
|
||||||
|
[25.3, 65.2, 45820, 3.2, 1013.2, 18.5, 45.3, 2.3]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, json=payload, headers=headers)
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
print(f"预测结果: {result['prediction']}")
|
||||||
|
print(f"置信度: {result['confidence']}%")`}</pre>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleTestModel}>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
测试推理
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { CheckCircle, Upload } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ModelService {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
type: string;
|
||||||
|
format: string;
|
||||||
|
description: string;
|
||||||
|
accessLevel: string;
|
||||||
|
tags: string[];
|
||||||
|
dependencies: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelEditDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
model: ModelService | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModelEditDialog({ open, onOpenChange, model }: ModelEditDialogProps) {
|
||||||
|
const handleSaveEdit = () => {
|
||||||
|
toast.success('模型信息已更新');
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!model) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>编辑模型信息 - {model.name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
修改模型的元信息和配置参数
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>模型名称</Label>
|
||||||
|
<Input defaultValue={model.name} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>模型版本</Label>
|
||||||
|
<Input defaultValue={model.version} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>模型类型</Label>
|
||||||
|
<Select defaultValue={model.type}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="作物生长预测">作物生长预测</SelectItem>
|
||||||
|
<SelectItem value="病虫害识别">病虫害识别</SelectItem>
|
||||||
|
<SelectItem value="产量预估">产量预估</SelectItem>
|
||||||
|
<SelectItem value="土壤分析">土壤分析</SelectItem>
|
||||||
|
<SelectItem value="灌溉优化">灌溉优化</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>模型格式</Label>
|
||||||
|
<Select defaultValue={model.format}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ONNX">ONNX</SelectItem>
|
||||||
|
<SelectItem value="TensorFlow">TensorFlow</SelectItem>
|
||||||
|
<SelectItem value="PyTorch">PyTorch</SelectItem>
|
||||||
|
<SelectItem value="Scikit-learn">Scikit-learn</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>模型描述</Label>
|
||||||
|
<Textarea defaultValue={model.description} rows={3} placeholder="描述模型的功能、适用场景等..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>模型文件</Label>
|
||||||
|
<div className="border-2 border-dashed rounded-lg p-6 text-center">
|
||||||
|
<Upload className="w-8 h-8 mx-auto text-muted-foreground mb-2" />
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">
|
||||||
|
点击上传或拖拽模型文件到此区域
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
支持 .onnx, .h5, .pb, .pt 等格式
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>依赖包</Label>
|
||||||
|
<Textarea defaultValue={model.dependencies.join('\n')} rows={3} placeholder="每行一个依赖,如:tensorflow==2.13.0" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>访问权限</Label>
|
||||||
|
<Select defaultValue={model.accessLevel}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="公开">公开</SelectItem>
|
||||||
|
<SelectItem value="私有">私有</SelectItem>
|
||||||
|
<SelectItem value="团队共享">团队共享</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>标签</Label>
|
||||||
|
<Input defaultValue={model.tags.join(', ')} placeholder="用逗号分隔,如:深度学习,CNN" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600" onClick={handleSaveEdit}>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
保存更改
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
Activity,
|
||||||
|
Zap,
|
||||||
|
Server,
|
||||||
|
Cpu,
|
||||||
|
BarChart3,
|
||||||
|
Eye,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface PerformanceMetrics {
|
||||||
|
avgResponseTime: number;
|
||||||
|
p95ResponseTime: number;
|
||||||
|
p99ResponseTime: number;
|
||||||
|
qps: number;
|
||||||
|
errorRate: number;
|
||||||
|
cpuUsage: number;
|
||||||
|
memoryUsage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelService {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PerformanceTuneDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
model: ModelService | null;
|
||||||
|
performanceMetrics: PerformanceMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PerformanceTuneDialog({ open, onOpenChange, model, performanceMetrics }: PerformanceTuneDialogProps) {
|
||||||
|
const handleApplyTuning = () => {
|
||||||
|
toast.success('性能优化配置已应用');
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!model) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>性能调优 - {model.name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
优化模型服务的性能和资源使用
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 当前性能指标 */}
|
||||||
|
<Card className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-950 dark:to-purple-950">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Activity className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
当前性能指标
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<div className="p-3 bg-white dark:bg-gray-900 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground">平均响应时间</p>
|
||||||
|
<p className="text-2xl text-blue-600 dark:text-blue-400 mt-1">{performanceMetrics.avgResponseTime}ms</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-white dark:bg-gray-900 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground">QPS</p>
|
||||||
|
<p className="text-2xl text-green-600 dark:text-green-400 mt-1">{performanceMetrics.qps}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-white dark:bg-gray-900 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground">CPU使用率</p>
|
||||||
|
<p className="text-2xl text-orange-600 dark:text-orange-400 mt-1">{performanceMetrics.cpuUsage}%</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-white dark:bg-gray-900 rounded-lg">
|
||||||
|
<p className="text-xs text-muted-foreground">内存使用率</p>
|
||||||
|
<p className="text-2xl text-purple-600 dark:text-purple-400 mt-1">{performanceMetrics.memoryUsage}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 负载均衡配置 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Zap className="w-4 h-4 text-orange-600 dark:text-orange-400" />
|
||||||
|
负载均衡策略
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>负载均衡算法</Label>
|
||||||
|
<Select defaultValue="round-robin">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="round-robin">轮询 (Round Robin)</SelectItem>
|
||||||
|
<SelectItem value="least-connections">最少连接 (Least Connections)</SelectItem>
|
||||||
|
<SelectItem value="ip-hash">IP哈希 (IP Hash)</SelectItem>
|
||||||
|
<SelectItem value="weighted">加权轮询 (Weighted)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>会话保持时间(秒)</Label>
|
||||||
|
<Input type="number" defaultValue="300" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>健康检查间隔(秒)</Label>
|
||||||
|
<Input type="number" defaultValue="30" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between p-3 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">启用跨区域负载均衡</div>
|
||||||
|
<div className="text-xs text-muted-foreground">在多个区域间分配流量</div>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 缓存配置 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Server className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||||
|
缓存优化
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between p-3 bg-purple-50 dark:bg-purple-950 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">启用结果缓存</div>
|
||||||
|
<div className="text-xs text-muted-foreground">缓存相同输入的推理结果</div>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>缓存策略</Label>
|
||||||
|
<Select defaultValue="lru">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="lru">LRU (最近最少使用)</SelectItem>
|
||||||
|
<SelectItem value="lfu">LFU (最不经常使用)</SelectItem>
|
||||||
|
<SelectItem value="fifo">FIFO (先进先出)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>缓存大小 (MB)</Label>
|
||||||
|
<Input type="number" defaultValue="1024" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>缓存过期时间(秒)</Label>
|
||||||
|
<Input type="number" defaultValue="3600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>最大缓存条目</Label>
|
||||||
|
<Input type="number" defaultValue="10000" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 并发控制 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Cpu className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
并发控制
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>最大并发请求数</Label>
|
||||||
|
<Input type="number" defaultValue="100" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>每实例并发数</Label>
|
||||||
|
<Input type="number" defaultValue="10" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>请求队列长度</Label>
|
||||||
|
<Input type="number" defaultValue="1000" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>队列超时(秒)</Label>
|
||||||
|
<Input type="number" defaultValue="60" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 资源限制 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4">资源限制优化</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm">内存限制</span>
|
||||||
|
<span className="font-medium">8GB</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={performanceMetrics.memoryUsage} className="h-2" />
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">当前使用: {performanceMetrics.memoryUsage}%</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm">CPU限制</span>
|
||||||
|
<span className="font-medium">4核</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={performanceMetrics.cpuUsage} className="h-2" />
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">当前使用: {performanceMetrics.cpuUsage}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 性能测试 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<BarChart3 className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||||
|
性能测试
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
运行压力测试以验证优化效果
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>并发用户数</Label>
|
||||||
|
<Input type="number" defaultValue="50" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>测试持续时间(分钟)</Label>
|
||||||
|
<Input type="number" defaultValue="5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>目标QPS</Label>
|
||||||
|
<Input type="number" defaultValue="100" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" className="w-full">
|
||||||
|
<Activity className="w-4 h-4 mr-2" />
|
||||||
|
开始压力测试
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline">
|
||||||
|
<Eye className="w-4 h-4 mr-2" />
|
||||||
|
预览配置
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600" onClick={handleApplyTuning}>
|
||||||
|
<Zap className="w-4 h-4 mr-2" />
|
||||||
|
应用优化配置
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
Shield,
|
||||||
|
Unlock,
|
||||||
|
Users,
|
||||||
|
Lock,
|
||||||
|
Gauge,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface ModelService {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
accessLevel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PermissionManageDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
model: ModelService | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PermissionManageDialog({ open, onOpenChange, model }: PermissionManageDialogProps) {
|
||||||
|
const handleSavePermission = () => {
|
||||||
|
toast.success('权限设置已保存');
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!model) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>权限管理 - {model.name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
配置模型服务的访问权限和使用限制
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 访问级别 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Shield className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
访问级别
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Unlock className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">公开访问</div>
|
||||||
|
<div className="text-xs text-muted-foreground">任何人都可以访问此模型</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="access"
|
||||||
|
defaultChecked={model.accessLevel === '公开'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">团队共享</div>
|
||||||
|
<div className="text-xs text-muted-foreground">仅团队成员可以访问</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="access"
|
||||||
|
defaultChecked={model.accessLevel === '团队共享'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Lock className="w-5 h-5 text-red-600 dark:text-red-400" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">私有访问</div>
|
||||||
|
<div className="text-xs text-muted-foreground">仅所有者可以访问</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="access"
|
||||||
|
defaultChecked={model.accessLevel === '私有'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* API限流 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Gauge className="w-4 h-4 text-orange-600 dark:text-orange-400" />
|
||||||
|
API限流配置
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>每秒请求数限制</Label>
|
||||||
|
<Input type="number" defaultValue="100" placeholder="0表示无限制" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>每天请求数限制</Label>
|
||||||
|
<Input type="number" defaultValue="10000" placeholder="0表示无限制" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>单次批量大小限制</Label>
|
||||||
|
<Input type="number" defaultValue="32" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>并发连接数限制</Label>
|
||||||
|
<Input type="number" defaultValue="10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* IP白名单 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Shield className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||||
|
IP白名单
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between p-3 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">启用IP白名单</div>
|
||||||
|
<div className="text-xs text-muted-foreground">仅允许白名单内的IP访问</div>
|
||||||
|
</div>
|
||||||
|
<Switch />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>IP地址列表</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="每行一个IP地址或CIDR,如:192.168.1.1 或 10.0.0.0/8"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 访问令牌管理 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4">访问令牌</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between p-3 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">启用API密钥认证</div>
|
||||||
|
<div className="text-xs text-muted-foreground">API调用需要有效的密钥</div>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>已生成的API密钥</Label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-900 rounded text-sm">
|
||||||
|
<code className="font-mono text-xs">sk-1234567890abcdef...</code>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button size="sm" variant="ghost">复制</Button>
|
||||||
|
<Button size="sm" variant="ghost">删除</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" variant="outline" className="w-full">
|
||||||
|
生成新密钥
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-purple-600 hover:bg-purple-700 dark:bg-purple-700 dark:hover:bg-purple-600" onClick={handleSavePermission}>
|
||||||
|
<Shield className="w-4 h-4 mr-2" />
|
||||||
|
保存权限设置
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Server } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ServiceConfigDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServiceConfigDialog({ open, onOpenChange }: ServiceConfigDialogProps) {
|
||||||
|
const handleSaveConfig = () => {
|
||||||
|
toast.success('配置已保存');
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>服务配置</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
配置模型服务的运行参数和性能优化选项
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>最大并发数</Label>
|
||||||
|
<Input type="number" placeholder="10" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>超时时间(秒)</Label>
|
||||||
|
<Input type="number" placeholder="30" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>重试次数</Label>
|
||||||
|
<Input type="number" placeholder="3" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>最小实例数</Label>
|
||||||
|
<Input type="number" placeholder="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">启用负载均衡</div>
|
||||||
|
<div className="text-xs text-muted-foreground">自动分配请求到多个实例</div>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-4 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">自动伸缩</div>
|
||||||
|
<div className="text-xs text-muted-foreground">根据负载自动调整实例数量</div>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>环境变量</Label>
|
||||||
|
<Textarea placeholder="KEY=VALUE,每行一个" rows={3} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600" onClick={handleSaveConfig}>
|
||||||
|
保存配置
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
GitBranch,
|
||||||
|
Plus,
|
||||||
|
CheckCircle,
|
||||||
|
Download,
|
||||||
|
RefreshCw,
|
||||||
|
BarChart3,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
LineChart as ReLineChart,
|
||||||
|
Line,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip as RechartsTooltip,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer,
|
||||||
|
} from 'recharts';
|
||||||
|
|
||||||
|
interface ModelService {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
lastUpdateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VersionManageDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
model: ModelService | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VersionManageDialog({ open, onOpenChange, model }: VersionManageDialogProps) {
|
||||||
|
const handleSwitchVersion = (version: string) => {
|
||||||
|
toast.success(`已切换到版本 ${version}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadVersion = (version: string) => {
|
||||||
|
toast.success(`开始下载版本 ${version}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!model) return null;
|
||||||
|
|
||||||
|
// 模拟版本数据
|
||||||
|
const versions = [
|
||||||
|
{ version: 'v2.3.1', date: '2024-10-10', accuracy: 94.5, inference: 120, status: '当前', desc: '优化推理性能,提升准确率' },
|
||||||
|
{ version: 'v2.3.0', date: '2024-09-15', accuracy: 93.8, inference: 135, status: '已归档', desc: '增加新特征,改进模型结构' },
|
||||||
|
{ version: 'v2.2.0', date: '2024-08-20', accuracy: 92.5, inference: 145, status: '已归档', desc: '数据集扩充,重新训练' },
|
||||||
|
{ version: 'v2.1.0', date: '2024-07-10', accuracy: 91.2, inference: 150, status: '已归档', desc: '修复已知问题,提升稳定性' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 版本性能对比数据
|
||||||
|
const performanceData = [
|
||||||
|
{ version: 'v2.1.0', 准确率: 91.2, 推理时间: 150 },
|
||||||
|
{ version: 'v2.2.0', 准确率: 92.5, 推理时间: 145 },
|
||||||
|
{ version: 'v2.3.0', 准确率: 93.8, 推理时间: 135 },
|
||||||
|
{ version: 'v2.3.1', 准确率: 94.5, 推理时间: 120 },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>版本管理 - {model.name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
管理模型的所有版本,支持版本切换和回滚
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 当前版本 */}
|
||||||
|
<Card className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 border-blue-200 dark:from-blue-950 dark:to-purple-950 dark:border-blue-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="flex items-center gap-2">
|
||||||
|
<GitBranch className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
当前版本
|
||||||
|
</h4>
|
||||||
|
<p className="text-2xl mt-2 font-mono">{model.version}</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
最后更新: {model.lastUpdateTime}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300">生产环境</Badge>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 版本列表 */}
|
||||||
|
<Card>
|
||||||
|
<div className="p-4 border-b">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4>历史版本</h4>
|
||||||
|
<Button size="sm" className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600">
|
||||||
|
<Plus className="w-3 h-3 mr-1" />
|
||||||
|
发布新版本
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>版本号</TableHead>
|
||||||
|
<TableHead>发布时间</TableHead>
|
||||||
|
<TableHead>准确率</TableHead>
|
||||||
|
<TableHead>推理时间</TableHead>
|
||||||
|
<TableHead>状态</TableHead>
|
||||||
|
<TableHead>描述</TableHead>
|
||||||
|
<TableHead>操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{versions.map((ver) => (
|
||||||
|
<TableRow key={ver.version}>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{ver.status === '当前' && <CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />}
|
||||||
|
<code className="font-mono">{ver.version}</code>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm">{ver.date}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-green-600 dark:text-green-400">{ver.accuracy}%</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{ver.inference}ms</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className={ver.status === '当前' ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300' : 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'}>
|
||||||
|
{ver.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs text-muted-foreground">
|
||||||
|
{ver.desc}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{ver.status !== '当前' && (
|
||||||
|
<Button size="sm" variant="outline" onClick={() => handleSwitchVersion(ver.version)}>
|
||||||
|
<RefreshCw className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button size="sm" variant="outline" onClick={() => handleDownloadVersion(ver.version)}>
|
||||||
|
<Download className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 版本对比 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<BarChart3 className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||||
|
版本性能对比
|
||||||
|
</h4>
|
||||||
|
<ResponsiveContainer width="100%" height={250}>
|
||||||
|
<ReLineChart data={performanceData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="version" />
|
||||||
|
<YAxis yAxisId="left" />
|
||||||
|
<YAxis yAxisId="right" orientation="right" />
|
||||||
|
<RechartsTooltip />
|
||||||
|
<Legend />
|
||||||
|
<Line yAxisId="left" type="monotone" dataKey="准确率" stroke="#10b981" strokeWidth={2} />
|
||||||
|
<Line yAxisId="right" type="monotone" dataKey="推理时间" stroke="#3b82f6" strokeWidth={2} />
|
||||||
|
</ReLineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-purple-600 hover:bg-purple-700 dark:bg-purple-700 dark:hover:bg-purple-600">
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
导出版本历史
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,759 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 模型接入页面 - AI模型统一接入管理平台
|
||||||
|
* 功能:模型服务注册、接入步骤说明、格式支持展示、快速接入示例、模型管理
|
||||||
|
* 路径:/ai-crop-model/model-integration/access
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,shadcn语义化样式,支持暗色主题
|
||||||
|
*/
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useReducer } from 'react';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { copyToClipboard } from '@/lib/clipboard';
|
||||||
|
import {
|
||||||
|
Brain,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
RefreshCw,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
AlertCircle,
|
||||||
|
PlayCircle,
|
||||||
|
PauseCircle,
|
||||||
|
Settings,
|
||||||
|
Eye,
|
||||||
|
Edit,
|
||||||
|
Trash2,
|
||||||
|
Link,
|
||||||
|
Cpu,
|
||||||
|
Zap,
|
||||||
|
Shield,
|
||||||
|
BarChart3,
|
||||||
|
Activity,
|
||||||
|
Clock,
|
||||||
|
Package,
|
||||||
|
Server,
|
||||||
|
Gauge,
|
||||||
|
TrendingUp,
|
||||||
|
Users,
|
||||||
|
Lock,
|
||||||
|
Unlock,
|
||||||
|
Copy,
|
||||||
|
GitBranch,
|
||||||
|
Code,
|
||||||
|
Terminal,
|
||||||
|
RotateCw,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// 导入弹窗组件
|
||||||
|
import { ServiceConfigDialog } from './components/ServiceConfigDialog';
|
||||||
|
import { ModelDetailDialog } from './components/ModelDetailDialog';
|
||||||
|
import { VersionManageDialog } from './components/VersionManageDialog';
|
||||||
|
import { DeployConfigDialog } from './components/DeployConfigDialog';
|
||||||
|
import { ModelEditDialog } from './components/ModelEditDialog';
|
||||||
|
import { PermissionManageDialog } from './components/PermissionManageDialog';
|
||||||
|
import { DependencyManageDialog } from './components/DependencyManageDialog';
|
||||||
|
import { PerformanceTuneDialog } from './components/PerformanceTuneDialog';
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
type ModelType = '作物生长预测' | '病虫害识别' | '产量预估' | '土壤分析' | '灌溉优化' | '其他';
|
||||||
|
type ModelFormat = 'ONNX' | 'TensorFlow' | 'PyTorch' | 'Scikit-learn' | 'H5' | 'SavedModel';
|
||||||
|
type ModelStatus = '运行中' | '已停止' | '部署中' | '故障' | '维护中';
|
||||||
|
type AccessLevel = '公开' | '私有' | '团队共享';
|
||||||
|
|
||||||
|
interface ModelService {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
type: ModelType;
|
||||||
|
format: ModelFormat;
|
||||||
|
description: string;
|
||||||
|
author: string;
|
||||||
|
createTime: string;
|
||||||
|
lastUpdateTime: string;
|
||||||
|
status: ModelStatus;
|
||||||
|
endpoint: string;
|
||||||
|
accessLevel: AccessLevel;
|
||||||
|
tags: string[];
|
||||||
|
accuracy?: number;
|
||||||
|
inferenceTime?: number;
|
||||||
|
requestCount: number;
|
||||||
|
successRate: number;
|
||||||
|
dependencies: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PerformanceMetrics {
|
||||||
|
avgResponseTime: number;
|
||||||
|
p95ResponseTime: number;
|
||||||
|
p99ResponseTime: number;
|
||||||
|
qps: number;
|
||||||
|
errorRate: number;
|
||||||
|
cpuUsage: number;
|
||||||
|
memoryUsage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelState {
|
||||||
|
modelServices: ModelService[];
|
||||||
|
selectedModel: ModelService | null;
|
||||||
|
showModelDialog: boolean;
|
||||||
|
showConfigDialog: boolean;
|
||||||
|
showDetailDialog: boolean;
|
||||||
|
showVersionDialog: boolean;
|
||||||
|
showDeployDialog: boolean;
|
||||||
|
showEditDialog: boolean;
|
||||||
|
showPermissionDialog: boolean;
|
||||||
|
showDependencyDialog: boolean;
|
||||||
|
showPerformanceDialog: boolean;
|
||||||
|
performanceMetrics: PerformanceMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelAction =
|
||||||
|
| { type: 'SET_MODEL_SERVICES'; payload: ModelService[] }
|
||||||
|
| { type: 'SET_SELECTED_MODEL'; payload: ModelService | null }
|
||||||
|
| { type: 'TOGGLE_DIALOG'; payload: keyof Pick<ModelState, 'showModelDialog' | 'showConfigDialog' | 'showDetailDialog' | 'showVersionDialog' | 'showDeployDialog' | 'showEditDialog' | 'showPermissionDialog' | 'showDependencyDialog' | 'showPerformanceDialog'> }
|
||||||
|
| { type: 'UPDATE_MODEL_STATUS'; payload: { id: string; status: ModelStatus } };
|
||||||
|
|
||||||
|
// 初始状态
|
||||||
|
const initialState: ModelState = {
|
||||||
|
modelServices: [
|
||||||
|
{
|
||||||
|
id: 'model-1',
|
||||||
|
name: '番茄生长预测模型',
|
||||||
|
version: 'v2.3.1',
|
||||||
|
type: '作物生长预测',
|
||||||
|
format: 'TensorFlow',
|
||||||
|
description: '基于深度学习的番茄生长周期预测模型,综合温湿度、光照等因素',
|
||||||
|
author: '农业AI研究院',
|
||||||
|
createTime: '2024-03-15',
|
||||||
|
lastUpdateTime: '2024-10-10',
|
||||||
|
status: '运行中',
|
||||||
|
endpoint: 'https://api.farm-ai.com/v1/tomato-growth',
|
||||||
|
accessLevel: '团队共享',
|
||||||
|
tags: ['作物生长', '番茄', '深度学习'],
|
||||||
|
accuracy: 94.5,
|
||||||
|
inferenceTime: 120,
|
||||||
|
requestCount: 15680,
|
||||||
|
successRate: 99.2,
|
||||||
|
dependencies: ['tensorflow==2.13.0', 'numpy==1.24.0', 'pandas==2.0.0'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'model-2',
|
||||||
|
name: '病虫害智能识别',
|
||||||
|
version: 'v1.8.0',
|
||||||
|
type: '病虫害识别',
|
||||||
|
format: 'PyTorch',
|
||||||
|
description: '基于卷积神经网络的作物病虫害图像识别模型,支持20+种常见病虫害',
|
||||||
|
author: '植保技术团队',
|
||||||
|
createTime: '2024-05-20',
|
||||||
|
lastUpdateTime: '2024-10-12',
|
||||||
|
status: '运行中',
|
||||||
|
endpoint: 'https://api.farm-ai.com/v1/pest-detection',
|
||||||
|
accessLevel: '公开',
|
||||||
|
tags: ['病虫害', '图像识别', 'CNN'],
|
||||||
|
accuracy: 96.8,
|
||||||
|
inferenceTime: 85,
|
||||||
|
requestCount: 28950,
|
||||||
|
successRate: 98.7,
|
||||||
|
dependencies: ['torch==2.0.1', 'torchvision==0.15.0', 'opencv-python==4.8.0'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'model-3',
|
||||||
|
name: '产量预估分析',
|
||||||
|
version: 'v3.1.2',
|
||||||
|
type: '产量预估',
|
||||||
|
format: 'ONNX',
|
||||||
|
description: '综合历史数据和实时监测的作物产量预估模型',
|
||||||
|
author: '数据分析中心',
|
||||||
|
createTime: '2024-01-10',
|
||||||
|
lastUpdateTime: '2024-09-28',
|
||||||
|
status: '运行中',
|
||||||
|
endpoint: 'https://api.farm-ai.com/v1/yield-prediction',
|
||||||
|
accessLevel: '私有',
|
||||||
|
tags: ['产量预估', 'LSTM', '时间序列'],
|
||||||
|
accuracy: 92.3,
|
||||||
|
inferenceTime: 150,
|
||||||
|
requestCount: 8520,
|
||||||
|
successRate: 99.5,
|
||||||
|
dependencies: ['onnxruntime==1.15.0', 'scikit-learn==1.3.0'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'model-4',
|
||||||
|
name: '土壤养分分析',
|
||||||
|
version: 'v2.0.0',
|
||||||
|
type: '土壤分析',
|
||||||
|
format: 'Scikit-learn',
|
||||||
|
description: '基于机器学习的土壤养分含量预测与分析模型',
|
||||||
|
author: '土壤实验室',
|
||||||
|
createTime: '2024-06-01',
|
||||||
|
lastUpdateTime: '2024-10-05',
|
||||||
|
status: '已停止',
|
||||||
|
endpoint: 'https://api.farm-ai.com/v1/soil-analysis',
|
||||||
|
accessLevel: '团队共享',
|
||||||
|
tags: ['土壤分析', '机器学习'],
|
||||||
|
accuracy: 89.7,
|
||||||
|
inferenceTime: 95,
|
||||||
|
requestCount: 4250,
|
||||||
|
successRate: 97.8,
|
||||||
|
dependencies: ['scikit-learn==1.3.0', 'xgboost==2.0.0'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedModel: null,
|
||||||
|
showModelDialog: false,
|
||||||
|
showConfigDialog: false,
|
||||||
|
showDetailDialog: false,
|
||||||
|
showVersionDialog: false,
|
||||||
|
showDeployDialog: false,
|
||||||
|
showEditDialog: false,
|
||||||
|
showPermissionDialog: false,
|
||||||
|
showDependencyDialog: false,
|
||||||
|
showPerformanceDialog: false,
|
||||||
|
performanceMetrics: {
|
||||||
|
avgResponseTime: 115,
|
||||||
|
p95ResponseTime: 280,
|
||||||
|
p99ResponseTime: 450,
|
||||||
|
qps: 45.6,
|
||||||
|
errorRate: 0.8,
|
||||||
|
cpuUsage: 42.5,
|
||||||
|
memoryUsage: 68.3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reducer
|
||||||
|
function modelReducer(state: ModelState, action: ModelAction): ModelState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_MODEL_SERVICES':
|
||||||
|
return { ...state, modelServices: action.payload };
|
||||||
|
case 'SET_SELECTED_MODEL':
|
||||||
|
return { ...state, selectedModel: action.payload };
|
||||||
|
case 'TOGGLE_DIALOG':
|
||||||
|
return { ...state, [action.payload]: !state[action.payload] };
|
||||||
|
case 'UPDATE_MODEL_STATUS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
modelServices: state.modelServices.map(model =>
|
||||||
|
model.id === action.payload.id
|
||||||
|
? { ...model, status: action.payload.status }
|
||||||
|
: model
|
||||||
|
),
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ModelAccessPage() {
|
||||||
|
const [state, dispatch] = useReducer(modelReducer, initialState);
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const totalModels = state.modelServices.length;
|
||||||
|
const runningModels = state.modelServices.filter(m => m.status === '运行中').length;
|
||||||
|
const stoppedModels = state.modelServices.filter(m => m.status === '已停止').length;
|
||||||
|
const avgAccuracy = state.modelServices.reduce((sum, m) => sum + (m.accuracy || 0), 0) / totalModels;
|
||||||
|
|
||||||
|
// 模型类型分布数据
|
||||||
|
const modelTypeDistribution = [
|
||||||
|
{ name: '作物生长预测', value: 3, color: '#10b981' },
|
||||||
|
{ name: '病虫害识别', value: 2, color: '#3b82f6' },
|
||||||
|
{ name: '产量预估', value: 2, color: '#f59e0b' },
|
||||||
|
{ name: '土壤分析', value: 1, color: '#8b5cf6' },
|
||||||
|
{ name: '灌溉优化', value: 1, color: '#ec4899' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 模型调用趋势数据
|
||||||
|
const modelCallTrend = [
|
||||||
|
{ time: '10:00', 调用次数: 120, 成功率: 98.5 },
|
||||||
|
{ time: '11:00', 调用次数: 156, 成功率: 99.1 },
|
||||||
|
{ time: '12:00', 调用次数: 142, 成功率: 98.8 },
|
||||||
|
{ time: '13:00', 调用次数: 178, 成功率: 99.3 },
|
||||||
|
{ time: '14:00', 调用次数: 195, 成功率: 99.0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
const getStatusColor = (status: ModelStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case '运行中': return 'bg-success-muted text-success-muted-foreground';
|
||||||
|
case '已停止': return 'bg-muted text-muted-foreground';
|
||||||
|
case '部署中': return 'bg-info-muted text-info-muted-foreground';
|
||||||
|
case '故障': return 'bg-error-muted text-error-muted-foreground';
|
||||||
|
case '维护中': return 'bg-warning-muted text-warning-muted-foreground';
|
||||||
|
default: return 'bg-muted text-muted-foreground';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status: ModelStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case '运行中': return <PlayCircle className="w-4 h-4 text-success" />;
|
||||||
|
case '已停止': return <PauseCircle className="w-4 h-4 text-muted-foreground" />;
|
||||||
|
case '部署中': return <RefreshCw className="w-4 h-4 text-info animate-spin" />;
|
||||||
|
case '故障': return <XCircle className="w-4 h-4 text-error" />;
|
||||||
|
case '维护中': return <AlertCircle className="w-4 h-4 text-warning" />;
|
||||||
|
default: return <AlertCircle className="w-4 h-4 text-muted-foreground" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAccessLevelIcon = (level: AccessLevel) => {
|
||||||
|
switch (level) {
|
||||||
|
case '公开': return <Unlock className="w-4 h-4 text-green-600" />;
|
||||||
|
case '私有': return <Lock className="w-4 h-4 text-red-600" />;
|
||||||
|
case '团队共享': return <Users className="w-4 h-4 text-blue-600" />;
|
||||||
|
default: return <Lock className="w-4 h-4 text-gray-600" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 事件处理函数
|
||||||
|
const handleToggleDialog = (dialog: keyof Pick<ModelState, 'showModelDialog' | 'showConfigDialog' | 'showDetailDialog' | 'showVersionDialog' | 'showDeployDialog' | 'showEditDialog' | 'showPermissionDialog' | 'showDependencyDialog' | 'showPerformanceDialog'>) => {
|
||||||
|
dispatch({ type: 'TOGGLE_DIALOG', payload: dialog });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectModel = (model: ModelService) => {
|
||||||
|
dispatch({ type: 'SET_SELECTED_MODEL', payload: model });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartModel = (modelName: string) => {
|
||||||
|
toast.success(`模型"${modelName}"已启动`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStopModel = (modelName: string) => {
|
||||||
|
toast.success(`模型"${modelName}"已停止`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestModel = () => {
|
||||||
|
toast.success('模型测试成功,推理正常');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveModel = () => {
|
||||||
|
toast.success('模型服务注册成功');
|
||||||
|
handleToggleDialog('showModelDialog');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyEndpoint = async (endpoint: string) => {
|
||||||
|
const success = await copyToClipboard(endpoint);
|
||||||
|
if (success) {
|
||||||
|
toast.success('端点已复制到剪贴板');
|
||||||
|
} else {
|
||||||
|
toast.error('复制失败,请重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditModel = (model: ModelService) => {
|
||||||
|
handleSelectModel(model);
|
||||||
|
handleToggleDialog('showEditDialog');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewDetail = (model: ModelService) => {
|
||||||
|
handleSelectModel(model);
|
||||||
|
handleToggleDialog('showDetailDialog');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewVersions = (model: ModelService) => {
|
||||||
|
handleSelectModel(model);
|
||||||
|
handleToggleDialog('showVersionDialog');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePermissionManage = (model: ModelService) => {
|
||||||
|
handleSelectModel(model);
|
||||||
|
handleToggleDialog('showPermissionDialog');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDependencyManage = (model: ModelService) => {
|
||||||
|
handleSelectModel(model);
|
||||||
|
handleToggleDialog('showDependencyDialog');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePerformanceTune = (model: ModelService) => {
|
||||||
|
handleSelectModel(model);
|
||||||
|
handleToggleDialog('showPerformanceDialog');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRedeploy = (model: ModelService) => {
|
||||||
|
toast.success(`模型"${model.name}"重新部署已启动,预计3-5分钟完成`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfigModel = (model: ModelService) => {
|
||||||
|
handleSelectModel(model);
|
||||||
|
handleToggleDialog('showConfigDialog');
|
||||||
|
};
|
||||||
|
|
||||||
export default function AccessPage() {
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="p-6">
|
{/* 页面标题 */}
|
||||||
<h2 className="text-xl font-semibold">模型接入</h2>
|
<div>
|
||||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
<h2 className="text-3xl font-bold tracking-tight">模型接入</h2>
|
||||||
<p className="text-sm">
|
<p className="text-muted-foreground mt-1">
|
||||||
<strong>页面路径:</strong> /ai-crop-model/model-integration/access
|
AI模型统一接入、管理与服务化部署平台
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">模型总数</p>
|
||||||
|
<p className="mt-2 text-3xl text-blue-600 dark:text-blue-400">{totalModels}</p>
|
||||||
|
<p className="text-xs text-blue-600 dark:text-blue-400 mt-1">已注册模型</p>
|
||||||
|
</div>
|
||||||
|
<Brain className="w-12 h-12 text-blue-600 dark:text-blue-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">运行中</p>
|
||||||
|
<p className="mt-2 text-3xl text-green-600 dark:text-green-400">{runningModels}</p>
|
||||||
|
<p className="text-xs text-green-600 dark:text-green-400 mt-1">服务正常</p>
|
||||||
|
</div>
|
||||||
|
<PlayCircle className="w-12 h-12 text-green-600 dark:text-green-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">平均准确率</p>
|
||||||
|
<p className="mt-2 text-3xl text-purple-600 dark:text-purple-400">{avgAccuracy.toFixed(1)}%</p>
|
||||||
|
<p className="text-xs text-purple-600 dark:text-purple-400 mt-1">模型性能</p>
|
||||||
|
</div>
|
||||||
|
<BarChart3 className="w-12 h-12 text-purple-600 dark:text-purple-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">QPS</p>
|
||||||
|
<p className="mt-2 text-3xl text-orange-600 dark:text-orange-400">{state.performanceMetrics.qps}</p>
|
||||||
|
<p className="text-xs text-orange-600 dark:text-orange-400 mt-1">每秒请求</p>
|
||||||
|
</div>
|
||||||
|
<Zap className="w-12 h-12 text-orange-600 dark:text-orange-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 功能说明卡片 */}
|
||||||
|
<Card className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 border-blue-200 dark:from-blue-950 dark:to-purple-950 dark:border-blue-800">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Brain className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-blue-900 dark:text-blue-100">
|
||||||
|
<p className="mb-2">模型服务接入功能:</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• <strong>标准化框架</strong>: 提供统一的模型接入标准,支持配置化注册</li>
|
||||||
|
<li>• <strong>多种模型</strong>: 支持作物生长预测、病虫害识别、产量预估等AI模型</li>
|
||||||
|
<li>• <strong>格式兼容</strong>: 支持ONNX、TensorFlow、PyTorch等主流框架</li>
|
||||||
|
<li>• <strong>版本管理</strong>: 完善的模型版本控制与回滚机制</li>
|
||||||
|
<li>• <strong>快速部署</strong>: 一键封装、部署,降低集成复杂度</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch({ type: 'SET_SELECTED_MODEL', payload: null });
|
||||||
|
handleToggleDialog('showModelDialog');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
注册新模型
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 接入步骤说明 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Code className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
模型接入步骤
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div className="text-center p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Upload className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<h5 className="mb-2">1. 上传模型</h5>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
上传模型文件或提供模型仓库地址
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-purple-50 dark:bg-purple-950 rounded-lg">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Settings className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<h5 className="mb-2">2. 配置参数</h5>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
填写模型元信息、输入输出格式等
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-green-50 dark:bg-green-950 rounded-lg">
|
||||||
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
|
<CheckCircle className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<h5 className="mb-2">3. 验证测试</h5>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
测试模型推理功能,验证输出结果
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-orange-50 dark:bg-orange-950 rounded-lg">
|
||||||
|
<div className="w-12 h-12 bg-orange-100 dark:bg-orange-900 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Server className="w-6 h-6 text-orange-600 dark:text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<h5 className="mb-2">4. 服务部署</h5>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
一键部署,生成API服务接口
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 模型格式支持 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Package className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||||
|
支持的模型格式
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
{['ONNX', 'TensorFlow', 'PyTorch', 'Scikit-learn', 'H5', 'SavedModel'].map((format) => (
|
||||||
|
<div key={format} className="flex items-center gap-3 p-3 bg-muted dark:bg-muted rounded-lg">
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{format}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">完全支持</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 快速接入示例 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h4 className="mb-4 flex items-center gap-2">
|
||||||
|
<Terminal className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||||
|
快速接入示例
|
||||||
|
</h4>
|
||||||
|
<div className="bg-foreground text-green-400 p-4 rounded-lg font-mono text-sm overflow-x-auto">
|
||||||
|
<pre>{`# 使用Python SDK快速注册模型
|
||||||
|
from farm_ai_sdk import ModelRegistry
|
||||||
|
|
||||||
|
# 1. 初始化注册器
|
||||||
|
registry = ModelRegistry(api_key="your_api_key")
|
||||||
|
|
||||||
|
# 2. 注册模型
|
||||||
|
model = registry.register(
|
||||||
|
name="番茄生长预测模型",
|
||||||
|
model_path="./tomato_growth_v2.onnx",
|
||||||
|
model_type="作物生长预测",
|
||||||
|
version="v2.3.1",
|
||||||
|
metadata={
|
||||||
|
"input_shape": "(batch, 10, 8)",
|
||||||
|
"output_shape": "(batch, 1)",
|
||||||
|
"framework": "ONNX",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 部署模型
|
||||||
|
deployment = registry.deploy(
|
||||||
|
model_id=model.id,
|
||||||
|
replicas=3,
|
||||||
|
enable_autoscaling=True
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"模型已部署: {deployment.endpoint}")`}</pre>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 模型注册对话框 */}
|
||||||
|
<Dialog open={state.showModelDialog} onOpenChange={() => handleToggleDialog('showModelDialog')}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>注册AI模型</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
填写模型基本信息和配置参数
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>模型名称</Label>
|
||||||
|
<Input placeholder="输入模型名称" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>模型版本</Label>
|
||||||
|
<Input placeholder="v1.0.0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>模型类型</Label>
|
||||||
|
<Select>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择模型类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="作物生长预测">作物生长预测</SelectItem>
|
||||||
|
<SelectItem value="病虫害识别">病虫害识别</SelectItem>
|
||||||
|
<SelectItem value="产量预估">产量预估</SelectItem>
|
||||||
|
<SelectItem value="土壤分析">土壤分析</SelectItem>
|
||||||
|
<SelectItem value="灌溉优化">灌溉优化</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>模型格式</Label>
|
||||||
|
<Select>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择模型格式" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ONNX">ONNX</SelectItem>
|
||||||
|
<SelectItem value="TensorFlow">TensorFlow</SelectItem>
|
||||||
|
<SelectItem value="PyTorch">PyTorch</SelectItem>
|
||||||
|
<SelectItem value="Scikit-learn">Scikit-learn</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>模型描述</Label>
|
||||||
|
<Textarea placeholder="描述模型的功能、适用场景等..." rows={3} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>模型文件</Label>
|
||||||
|
<div className="border-2 border-dashed rounded-lg p-6 text-center">
|
||||||
|
<Upload className="w-8 h-8 mx-auto text-muted-foreground mb-2" />
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">
|
||||||
|
点击上传或拖拽模型文件到此区域
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
支持 .onnx, .h5, .pb, .pt 等格式
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>依赖包</Label>
|
||||||
|
<Textarea placeholder="每行一个依赖,如:tensorflow==2.13.0" rows={3} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>访问权限</Label>
|
||||||
|
<Select>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择访问权限" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="公开">公开</SelectItem>
|
||||||
|
<SelectItem value="私有">私有</SelectItem>
|
||||||
|
<SelectItem value="团队共享">团队共享</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>标签</Label>
|
||||||
|
<Input placeholder="用逗号分隔,如:深度学习,CNN" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => handleToggleDialog('showModelDialog')}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleTestModel}>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
测试模型
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600" onClick={handleSaveModel}>
|
||||||
|
<Server className="w-4 h-4 mr-2" />
|
||||||
|
注册并部署
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 所有弹窗组件 */}
|
||||||
|
<ServiceConfigDialog
|
||||||
|
open={state.showConfigDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) handleToggleDialog('showConfigDialog');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModelDetailDialog
|
||||||
|
open={state.showDetailDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) handleToggleDialog('showDetailDialog');
|
||||||
|
}}
|
||||||
|
model={state.selectedModel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VersionManageDialog
|
||||||
|
open={state.showVersionDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) handleToggleDialog('showVersionDialog');
|
||||||
|
}}
|
||||||
|
model={state.selectedModel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeployConfigDialog
|
||||||
|
open={state.showDeployDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) handleToggleDialog('showDeployDialog');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModelEditDialog
|
||||||
|
open={state.showEditDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) handleToggleDialog('showEditDialog');
|
||||||
|
}}
|
||||||
|
model={state.selectedModel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PermissionManageDialog
|
||||||
|
open={state.showPermissionDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) handleToggleDialog('showPermissionDialog');
|
||||||
|
}}
|
||||||
|
model={state.selectedModel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DependencyManageDialog
|
||||||
|
open={state.showDependencyDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) handleToggleDialog('showDependencyDialog');
|
||||||
|
}}
|
||||||
|
model={state.selectedModel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PerformanceTuneDialog
|
||||||
|
open={state.showPerformanceDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) handleToggleDialog('showPerformanceDialog');
|
||||||
|
}}
|
||||||
|
model={state.selectedModel}
|
||||||
|
performanceMetrics={state.performanceMetrics}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 模型注册对话框组件 - 新增AI模型注册
|
||||||
|
* 功能:模型基本信息填写、文件上传、权限配置、依赖管理
|
||||||
|
* 路径:/ai-crop-model/model-integration/management
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Upload, Save } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface AddModelDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
dispatch: React.Dispatch<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddModelDialog({ open, onOpenChange, dispatch }: AddModelDialogProps) {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
version: '',
|
||||||
|
type: '',
|
||||||
|
format: '',
|
||||||
|
description: '',
|
||||||
|
access: '',
|
||||||
|
tags: '',
|
||||||
|
dependencies: ''
|
||||||
|
});
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleInputChange = (field: string, value: string) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
// 验证必填项
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
toast.error('请输入模型名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.version.trim()) {
|
||||||
|
toast.error('请输入模型版本');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.type) {
|
||||||
|
toast.error('请选择模型类型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.format) {
|
||||||
|
toast.error('请选择模型格式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟API调用延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// 创建新模型对象
|
||||||
|
const newModel = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: formData.name,
|
||||||
|
type: formData.type,
|
||||||
|
version: formData.version,
|
||||||
|
status: 'active',
|
||||||
|
description: formData.description,
|
||||||
|
apiEndpoint: `https://api.smart-crop.com/v2/models/${formData.name.toLowerCase().replace(/\s+/g, '-')}`,
|
||||||
|
parameters: {
|
||||||
|
confidence_threshold: 0.85,
|
||||||
|
max_detections: 10
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
total_calls: 0,
|
||||||
|
avg_response_time: 200,
|
||||||
|
success_rate: 100,
|
||||||
|
accuracy: 95.0,
|
||||||
|
last_called: new Date().toISOString(),
|
||||||
|
qps: 0
|
||||||
|
},
|
||||||
|
tags: formData.tags ? formData.tags.split(',').map(tag => tag.trim()) : [],
|
||||||
|
visibility: formData.access === '公开' ? 'public' : formData.access === '团队共享' ? 'team' : 'private',
|
||||||
|
team: 'AI算法团队',
|
||||||
|
concurrency: 100,
|
||||||
|
timeout: 30000,
|
||||||
|
retryCount: 3,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加模型
|
||||||
|
dispatch({ type: 'ADD_MODEL', payload: newModel });
|
||||||
|
dispatch({ type: 'SET_ADD_MODEL_DIALOG', payload: false });
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
version: '',
|
||||||
|
type: '',
|
||||||
|
format: '',
|
||||||
|
description: '',
|
||||||
|
access: '',
|
||||||
|
tags: '',
|
||||||
|
dependencies: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success('模型注册成功!');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('注册模型失败,请重试');
|
||||||
|
console.error('Add model error:', error);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>注册AI模型</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
填写模型基本信息和配置参数
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>模型名称</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="输入模型名称"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>模型版本</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="v1.0.0"
|
||||||
|
value={formData.version}
|
||||||
|
onChange={(e) => handleInputChange('version', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>模型类型</Label>
|
||||||
|
<Select value={formData.type} onValueChange={(value) => handleInputChange('type', value)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择模型类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="pest_identification">病虫害识别</SelectItem>
|
||||||
|
<SelectItem value="growth_status">生长状态</SelectItem>
|
||||||
|
<SelectItem value="yield_prediction">产量预估</SelectItem>
|
||||||
|
<SelectItem value="soil_analysis">土壤分析</SelectItem>
|
||||||
|
<SelectItem value="irrigation_optimization">灌溉优化</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>模型格式</Label>
|
||||||
|
<Select value={formData.format} onValueChange={(value) => handleInputChange('format', value)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择模型格式" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ONNX">ONNX</SelectItem>
|
||||||
|
<SelectItem value="TensorFlow">TensorFlow</SelectItem>
|
||||||
|
<SelectItem value="PyTorch">PyTorch</SelectItem>
|
||||||
|
<SelectItem value="Scikit-learn">Scikit-learn</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>模型描述</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="描述模型的功能、适用场景等..."
|
||||||
|
rows={3}
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>模型文件</Label>
|
||||||
|
<div className="border-2 border-dashed rounded-lg p-6 text-center">
|
||||||
|
<Upload className="w-8 h-8 mx-auto text-muted-foreground mb-2" />
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">
|
||||||
|
点击上传或拖拽模型文件到此区域
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
支持 .onnx, .h5, .pb, .pt 等格式
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>依赖包</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="每行一个依赖,如:tensorflow==2.13.0"
|
||||||
|
rows={3}
|
||||||
|
value={formData.dependencies}
|
||||||
|
onChange={(e) => handleInputChange('dependencies', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>访问权限</Label>
|
||||||
|
<Select value={formData.access} onValueChange={(value) => handleInputChange('access', value)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择访问权限" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="公开">公开</SelectItem>
|
||||||
|
<SelectItem value="私有">私有</SelectItem>
|
||||||
|
<SelectItem value="团队共享">团队共享</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>标签</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="用逗号分隔,如:深度学习,CNN"
|
||||||
|
value={formData.tags}
|
||||||
|
onChange={(e) => handleInputChange('tags', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? '注册中...' : (
|
||||||
|
<>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
注册模型
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 删除模型确认对话框 - 模型服务删除确认界面
|
||||||
|
* 功能:删除确认、模型信息展示、风险提示、删除处理
|
||||||
|
* 路径:/ai-crop-model/model-integration/management
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用AlertDialog组件,安全删除确认
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { ModelService } from '../types';
|
||||||
|
import { AlertTriangle, Trash2, ExternalLink } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface DeleteModelDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
model: ModelService | null;
|
||||||
|
dispatch: React.Dispatch<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getModelTypeLabel = (type: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
'image_classification': '图像分类',
|
||||||
|
'object_detection': '目标检测',
|
||||||
|
'regression': '回归预测',
|
||||||
|
'multiclass_classification': '多类分类',
|
||||||
|
'optimization': '优化算法',
|
||||||
|
'time_series': '时序分析',
|
||||||
|
'nlp': '自然语言处理',
|
||||||
|
'anomaly_detection': '异常检测'
|
||||||
|
};
|
||||||
|
return labels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string) => {
|
||||||
|
const statusConfig = {
|
||||||
|
active: {
|
||||||
|
label: '运行中',
|
||||||
|
className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||||
|
},
|
||||||
|
inactive: {
|
||||||
|
label: '已停止',
|
||||||
|
className: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'
|
||||||
|
},
|
||||||
|
testing: {
|
||||||
|
label: '测试中',
|
||||||
|
className: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
label: '错误',
|
||||||
|
className: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = statusConfig[status as keyof typeof statusConfig];
|
||||||
|
return (
|
||||||
|
<Badge className={config.className}>
|
||||||
|
{config.label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DeleteModelDialog({ open, onOpenChange, model, dispatch }: DeleteModelDialogProps) {
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!model) return;
|
||||||
|
|
||||||
|
setIsDeleting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟API调用延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
// 删除模型
|
||||||
|
dispatch({ type: 'DELETE_MODEL', payload: model.id });
|
||||||
|
dispatch({ type: 'SET_DELETE_DIALOG', payload: false });
|
||||||
|
|
||||||
|
toast.success(`模型 "${model.name}" 已成功删除`);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('删除模型失败,请重试');
|
||||||
|
console.error('Delete model error:', error);
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!model) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialogContent className="max-w-lg">
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900">
|
||||||
|
<Trash2 className="h-6 w-6 text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<AlertDialogTitle className="text-red-600 dark:text-red-400">
|
||||||
|
删除模型确认
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="text-left mt-2">
|
||||||
|
您确定要删除这个AI模型服务吗?此操作不可撤销。
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<div className="my-6">
|
||||||
|
{/* 模型信息卡片 */}
|
||||||
|
<Card className="p-4 bg-muted/30 border-red-200 dark:border-red-800">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="font-medium text-foreground">模型信息</h4>
|
||||||
|
{getStatusBadge(model.status)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">模型名称:</span>
|
||||||
|
<span className="font-medium text-foreground">{model.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">模型类型:</span>
|
||||||
|
<span className="font-medium text-foreground">{getModelTypeLabel(model.type)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">版本:</span>
|
||||||
|
<span className="font-mono text-foreground">{model.version}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">API端点:</span>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<span className="font-mono text-xs text-foreground max-w-xs truncate">
|
||||||
|
{model.apiEndpoint}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-5 w-5 p-0"
|
||||||
|
onClick={() => window.open(model.apiEndpoint, '_blank')}
|
||||||
|
title="访问API"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-3 border-t border-border">
|
||||||
|
<div className="grid grid-cols-3 gap-3 text-center">
|
||||||
|
<div>
|
||||||
|
<div className="text-lg font-bold text-blue-600 dark:text-blue-400">
|
||||||
|
{model.metrics.total_calls.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">总调用次数</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-lg font-bold text-green-600 dark:text-green-400">
|
||||||
|
{model.metrics.accuracy.toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">准确率</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-lg font-bold text-orange-600 dark:text-orange-400">
|
||||||
|
{model.metrics.qps.toFixed(1)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">QPS</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 删除影响提示 */}
|
||||||
|
<div className="mt-4 p-3 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
|
||||||
|
<div className="flex items-start space-x-2">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||||
|
<div className="text-sm text-red-800 dark:text-red-200">
|
||||||
|
<p className="font-medium mb-1">删除后将会:</p>
|
||||||
|
<ul className="space-y-1 text-xs">
|
||||||
|
<li>• 永久删除模型配置和参数设置</li>
|
||||||
|
<li>• 停止所有相关的API调用服务</li>
|
||||||
|
<li>• 清除该模型的运行统计数据</li>
|
||||||
|
<li>• 影响依赖此模型的业务功能</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 创建时间信息 */}
|
||||||
|
<div className="mt-3 text-xs text-muted-foreground">
|
||||||
|
<p>
|
||||||
|
创建时间: {new Date(model.createdAt).toLocaleString('zh-CN')}
|
||||||
|
{model.updatedAt !== model.createdAt && (
|
||||||
|
<span className="ml-2">
|
||||||
|
| 最后更新: {new Date(model.updatedAt).toLocaleString('zh-CN')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={isDeleting}>
|
||||||
|
取消
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="bg-red-600 hover:bg-red-700 text-white"
|
||||||
|
>
|
||||||
|
{isDeleting ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
|
||||||
|
删除中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
确认删除
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 模型分析图表组件 - 模型调用趋势和类型分布
|
||||||
|
* 功能:展示模型调用趋势图表、模型类型分布饼图
|
||||||
|
* 路径:/ai-crop-model/model-integration/management
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip as RechartsTooltip,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer,
|
||||||
|
PieChart,
|
||||||
|
Pie,
|
||||||
|
Cell,
|
||||||
|
BarChart,
|
||||||
|
Bar
|
||||||
|
} from 'recharts';
|
||||||
|
import { TrendingUp, PieChart as PieChartIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ModelAnalyticsProps {
|
||||||
|
models: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getModelTypeLabel = (type: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
'pest_identification': '病虫害识别',
|
||||||
|
'growth_status': '生长状态',
|
||||||
|
'yield_prediction': '产量预测'
|
||||||
|
};
|
||||||
|
return labels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ModelAnalytics({ models }: ModelAnalyticsProps) {
|
||||||
|
// 模拟调用趋势数据 (最近7天)
|
||||||
|
const callTrendData = [
|
||||||
|
{ date: '03-15', calls: 12500, success: 12200, failed: 300 },
|
||||||
|
{ date: '03-16', calls: 13800, success: 13500, failed: 300 },
|
||||||
|
{ date: '03-17', calls: 14200, success: 13850, failed: 350 },
|
||||||
|
{ date: '03-18', calls: 15600, success: 15200, failed: 400 },
|
||||||
|
{ date: '03-19', calls: 16800, success: 16400, failed: 400 },
|
||||||
|
{ date: '03-20', calls: 18900, success: 18450, failed: 450 },
|
||||||
|
{ date: '03-21', calls: 21300, success: 20800, failed: 500 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 模型类型分布数据
|
||||||
|
const modelTypeData = models.reduce((acc, model) => {
|
||||||
|
const existing = acc.find(item => item.type === model.type);
|
||||||
|
if (existing) {
|
||||||
|
existing.count += 1;
|
||||||
|
} else {
|
||||||
|
acc.push({
|
||||||
|
type: model.type,
|
||||||
|
count: 1,
|
||||||
|
name: getModelTypeLabel(model.type)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as { type: string; count: number; name: string }[]);
|
||||||
|
|
||||||
|
|
||||||
|
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* 模型调用趋势 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<TrendingUp className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
<h3 className="text-lg font-semibold text-foreground">模型调用趋势</h3>
|
||||||
|
</div>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<LineChart data={callTrendData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="date" />
|
||||||
|
<YAxis />
|
||||||
|
<RechartsTooltip />
|
||||||
|
<Legend />
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="success"
|
||||||
|
stroke="#10b981"
|
||||||
|
strokeWidth={2}
|
||||||
|
name="成功调用"
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="failed"
|
||||||
|
stroke="#ef4444"
|
||||||
|
strokeWidth={2}
|
||||||
|
name="失败调用"
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="calls"
|
||||||
|
stroke="#3b82f6"
|
||||||
|
strokeWidth={2}
|
||||||
|
name="总调用"
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 模型类型分布 */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<PieChartIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||||
|
<h3 className="text-lg font-semibold text-foreground">模型类型分布</h3>
|
||||||
|
</div>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={modelTypeData}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
labelLine={false}
|
||||||
|
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||||
|
outerRadius={80}
|
||||||
|
fill="#8884d8"
|
||||||
|
dataKey="count"
|
||||||
|
>
|
||||||
|
{modelTypeData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<RechartsTooltip />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,450 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 模型配置对话框组件 - 模型编辑与查看界面
|
||||||
|
* 功能:模型信息编辑、参数配置、查看模式、保存处理
|
||||||
|
* 路径:/ai-crop-model/model-integration/management
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,支持编辑/查看双模式,shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { ModelService, ModelType, ModelStatus } from '../types';
|
||||||
|
import { Edit, Eye, Save, Copy, ExternalLink } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface ModelConfigDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
model: ModelService | null;
|
||||||
|
viewMode?: boolean;
|
||||||
|
dispatch: React.Dispatch<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MODEL_TYPES: { value: ModelType; label: string; description: string }[] = [
|
||||||
|
{
|
||||||
|
value: 'pest_identification',
|
||||||
|
label: '病虫害识别',
|
||||||
|
description: '识别农作物病虫害'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'growth_status',
|
||||||
|
label: '生长状态',
|
||||||
|
description: '分析作物生长阶段'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'yield_prediction',
|
||||||
|
label: '产量预测',
|
||||||
|
description: '预测农作物产量'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const STATUS_OPTIONS: { value: ModelStatus; label: string; color: string }[] = [
|
||||||
|
{ value: 'active', label: '运行中', color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
||||||
|
{ value: 'inactive', label: '已停止', color: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200' },
|
||||||
|
{ value: 'testing', label: '测试中', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
|
||||||
|
{ value: 'error', label: '错误', color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ModelConfigDialog({ open, onOpenChange, model, viewMode = false, dispatch }: ModelConfigDialogProps) {
|
||||||
|
const [formData, setFormData] = useState<Partial<ModelService>>({});
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && model) {
|
||||||
|
setFormData(model);
|
||||||
|
}
|
||||||
|
}, [open, model]);
|
||||||
|
|
||||||
|
const handleInputChange = (field: keyof ModelService, value: string) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
toast.success('已复制到剪贴板');
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
if (!formData.name?.trim()) {
|
||||||
|
toast.error('请输入模型名称');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.description?.trim()) {
|
||||||
|
toast.error('请输入模型描述');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.apiEndpoint?.trim()) {
|
||||||
|
toast.error('请输入API端点地址');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (viewMode) return;
|
||||||
|
|
||||||
|
if (!validateForm()) return;
|
||||||
|
if (!model) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新模型对象
|
||||||
|
const updatedModel: ModelService = {
|
||||||
|
...model,
|
||||||
|
...formData,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
} as ModelService;
|
||||||
|
|
||||||
|
// 模拟API调用延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// 更新模型
|
||||||
|
dispatch({ type: 'UPDATE_MODEL', payload: updatedModel });
|
||||||
|
dispatch({ type: 'SET_CONFIG_DIALOG', payload: false });
|
||||||
|
|
||||||
|
toast.success('模型更新成功!');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('更新模型失败,请重试');
|
||||||
|
console.error('Update model error:', error);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!model) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{viewMode ? (
|
||||||
|
<Eye className="h-5 w-5 text-blue-500" />
|
||||||
|
) : (
|
||||||
|
<Edit className="h-5 w-5 text-orange-500" />
|
||||||
|
)}
|
||||||
|
<DialogTitle>
|
||||||
|
{viewMode ? '查看模型详情' : '编辑模型配置'}
|
||||||
|
</DialogTitle>
|
||||||
|
</div>
|
||||||
|
<DialogDescription>
|
||||||
|
{viewMode
|
||||||
|
? '查看模型的详细信息和配置参数'
|
||||||
|
: '修改模型的基本信息和参数配置'
|
||||||
|
}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
{/* 模型基本信息 */}
|
||||||
|
<Card className="p-4 bg-muted/30">
|
||||||
|
<h4 className="font-medium text-foreground mb-4">基本信息</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">模型名称</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={formData.name || ''}
|
||||||
|
onChange={(e) => !viewMode && handleInputChange('name', e.target.value)}
|
||||||
|
disabled={viewMode}
|
||||||
|
placeholder="模型名称"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="type">模型类型</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.type}
|
||||||
|
onValueChange={(value: ModelType) => !viewMode && handleInputChange('type', value)}
|
||||||
|
disabled={viewMode}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{MODEL_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<span>{type.label}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{type.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="version">版本号</Label>
|
||||||
|
<Input
|
||||||
|
id="version"
|
||||||
|
value={formData.version || ''}
|
||||||
|
onChange={(e) => !viewMode && handleInputChange('version', e.target.value)}
|
||||||
|
disabled={viewMode}
|
||||||
|
placeholder="例如:1.0.0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="status">状态</Label>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Select
|
||||||
|
value={formData.status}
|
||||||
|
onValueChange={(value: ModelStatus) => !viewMode && handleInputChange('status', value)}
|
||||||
|
disabled={viewMode}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{STATUS_OPTIONS.map((status) => (
|
||||||
|
<SelectItem key={status.value} value={status.value}>
|
||||||
|
<Badge className={status.color}>
|
||||||
|
{status.label}
|
||||||
|
</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 col-span-2">
|
||||||
|
<Label htmlFor="apiEndpoint">API端点地址</Label>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Input
|
||||||
|
id="apiEndpoint"
|
||||||
|
value={formData.apiEndpoint || ''}
|
||||||
|
onChange={(e) => !viewMode && handleInputChange('apiEndpoint', e.target.value)}
|
||||||
|
disabled={viewMode}
|
||||||
|
placeholder="https://api.example.com/v1/models/..."
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => copyToClipboard(formData.apiEndpoint || '')}
|
||||||
|
title="复制API地址"
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => window.open(formData.apiEndpoint, '_blank')}
|
||||||
|
title="访问API"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 col-span-2">
|
||||||
|
<Label htmlFor="description">模型描述</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
value={formData.description || ''}
|
||||||
|
onChange={(e) => !viewMode && handleInputChange('description', e.target.value)}
|
||||||
|
disabled={viewMode}
|
||||||
|
placeholder="描述模型的功能、应用场景、技术特点等..."
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 服务配置 */}
|
||||||
|
<Card className="p-4 bg-muted/30">
|
||||||
|
<h4 className="font-medium text-foreground mb-4">服务配置</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="concurrency">并发数</Label>
|
||||||
|
<Input
|
||||||
|
id="concurrency"
|
||||||
|
type="number"
|
||||||
|
value={formData.concurrency || ''}
|
||||||
|
onChange={(e) => !viewMode && handleInputChange('concurrency', e.target.value)}
|
||||||
|
disabled={viewMode}
|
||||||
|
placeholder="100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="timeout">超时时间(ms)</Label>
|
||||||
|
<Input
|
||||||
|
id="timeout"
|
||||||
|
type="number"
|
||||||
|
value={formData.timeout || ''}
|
||||||
|
onChange={(e) => !viewMode && handleInputChange('timeout', e.target.value)}
|
||||||
|
disabled={viewMode}
|
||||||
|
placeholder="30000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="retryCount">重试次数</Label>
|
||||||
|
<Input
|
||||||
|
id="retryCount"
|
||||||
|
type="number"
|
||||||
|
value={formData.retryCount || ''}
|
||||||
|
onChange={(e) => !viewMode && handleInputChange('retryCount', e.target.value)}
|
||||||
|
disabled={viewMode}
|
||||||
|
placeholder="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="team">所属团队</Label>
|
||||||
|
<Input
|
||||||
|
id="team"
|
||||||
|
value={formData.team || ''}
|
||||||
|
onChange={(e) => !viewMode && handleInputChange('team', e.target.value)}
|
||||||
|
disabled={viewMode}
|
||||||
|
placeholder="AI算法团队"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 查询参数 */}
|
||||||
|
<Card className="p-4 bg-muted/30">
|
||||||
|
<h4 className="font-medium text-foreground mb-4">查询参数</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Object.entries(model.parameters).map(([key, value]) => (
|
||||||
|
<div key={key} className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-foreground">{key}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{typeof value === 'object' ? JSON.stringify(value) : value.toString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => copyToClipboard(
|
||||||
|
typeof value === 'object' ? JSON.stringify(value) : value.toString()
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 操作日志 */}
|
||||||
|
<Card className="p-4 bg-muted/30">
|
||||||
|
<h4 className="font-medium text-foreground mb-4">操作</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-foreground">模型部署</div>
|
||||||
|
<div className="text-sm text-muted-foreground">2024-03-20 14:30:00 by admin</div>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-green-100 text-green-800">成功</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-foreground">参数更新</div>
|
||||||
|
<div className="text-sm text-muted-foreground">2024-03-19 10:15:00 by admin</div>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-blue-100 text-blue-800">配置</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-foreground">版本升级</div>
|
||||||
|
<div className="text-sm text-muted-foreground">2024-03-18 16:45:00 by admin</div>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-purple-100 text-purple-800">更新</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-foreground">模型训练</div>
|
||||||
|
<div className="text-sm text-muted-foreground">2024-03-15 08:00:00 by admin</div>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-orange-100 text-orange-800">训练</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 运行统计信息 */}
|
||||||
|
<Card className="p-4 bg-muted/30">
|
||||||
|
<h4 className="font-medium text-foreground mb-4">运行统计</h4>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
||||||
|
{model.metrics.total_calls.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">总调用次数</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||||
|
{model.metrics.accuracy.toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">准确率</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-orange-600 dark:text-orange-400">
|
||||||
|
{model.metrics.qps.toFixed(1)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">QPS</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-purple-600 dark:text-purple-400">
|
||||||
|
{model.metrics.success_rate}%
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">成功率</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
{viewMode ? '关闭' : '取消'}
|
||||||
|
</Button>
|
||||||
|
{!viewMode && (
|
||||||
|
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? '保存中...' : (
|
||||||
|
<>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
保存更改
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 模型服务列表组件 - 模型服务展示与管理
|
||||||
|
* 功能:模型列表展示、状态切换、多操作按钮、分页功能
|
||||||
|
* 路径:/ai-crop-model/model-integration/management
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ModelService } from '../types';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import {
|
||||||
|
Eye,
|
||||||
|
Edit,
|
||||||
|
Settings,
|
||||||
|
Users,
|
||||||
|
Zap,
|
||||||
|
RotateCcw,
|
||||||
|
Power,
|
||||||
|
Trash2
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { ExternalLink } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ModelServiceListProps {
|
||||||
|
models: ModelService[];
|
||||||
|
onEdit: (model: ModelService) => void;
|
||||||
|
onView: (model: ModelService) => void;
|
||||||
|
onDelete: (model: ModelService) => void;
|
||||||
|
onToggle: (modelId: string) => void;
|
||||||
|
onPermission: (model: ModelService) => void;
|
||||||
|
onPerformance: (model: ModelService) => void;
|
||||||
|
onRedeploy: (model: ModelService) => void;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModelServiceList({
|
||||||
|
models,
|
||||||
|
onEdit,
|
||||||
|
onView,
|
||||||
|
onDelete,
|
||||||
|
onToggle,
|
||||||
|
onPermission,
|
||||||
|
onPerformance,
|
||||||
|
onRedeploy,
|
||||||
|
loading
|
||||||
|
}: ModelServiceListProps) {
|
||||||
|
const getModelTypeLabel = (type: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
'pest_identification': '病虫害识别',
|
||||||
|
'growth_status': '生长状态',
|
||||||
|
'yield_prediction': '产量预测'
|
||||||
|
};
|
||||||
|
return labels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string) => {
|
||||||
|
const statusConfig = {
|
||||||
|
active: {
|
||||||
|
label: '运行中',
|
||||||
|
className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||||
|
},
|
||||||
|
inactive: {
|
||||||
|
label: '已停止',
|
||||||
|
className: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'
|
||||||
|
},
|
||||||
|
testing: {
|
||||||
|
label: '测试中',
|
||||||
|
className: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
label: '错误',
|
||||||
|
className: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = statusConfig[status as keyof typeof statusConfig];
|
||||||
|
return (
|
||||||
|
<Badge className={config.className}>
|
||||||
|
{config.label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVisibilityBadge = (visibility: string) => {
|
||||||
|
const visibilityConfig = {
|
||||||
|
public: {
|
||||||
|
label: '公开',
|
||||||
|
className: 'bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300'
|
||||||
|
},
|
||||||
|
private: {
|
||||||
|
label: '私有',
|
||||||
|
className: 'bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-300'
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
label: '团队',
|
||||||
|
className: 'bg-blue-50 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = visibilityConfig[visibility as keyof typeof visibilityConfig];
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className={config.className}>
|
||||||
|
{config.label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now.getTime() - date.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||||||
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||||
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffMins < 60) {
|
||||||
|
return `${diffMins}分钟前`;
|
||||||
|
} else if (diffHours < 24) {
|
||||||
|
return `${diffHours}小时前`;
|
||||||
|
} else if (diffDays < 7) {
|
||||||
|
return `${diffDays}天前`;
|
||||||
|
} else {
|
||||||
|
return date.toLocaleDateString('zh-CN');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Card className="p-8">
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
|
<span className="ml-3 text-muted-foreground">加载模型服务...</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (models.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="p-8">
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="text-6xl mb-4">🤖</div>
|
||||||
|
<h3 className="text-lg font-medium text-muted-foreground mb-2">
|
||||||
|
暂无模型服务
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
点击"新增模型"按钮添加您的第一个AI模型服务
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-card">
|
||||||
|
<div className="p-6 border-b border-border">
|
||||||
|
<h3 className="text-lg font-semibold text-foreground">模型服务列表</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
管理和监控所有AI模型服务的运行状态
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>模型名称</TableHead>
|
||||||
|
<TableHead>类型/格式</TableHead>
|
||||||
|
<TableHead>版本</TableHead>
|
||||||
|
<TableHead>性能指标</TableHead>
|
||||||
|
<TableHead>调用统计</TableHead>
|
||||||
|
<TableHead>访问权限</TableHead>
|
||||||
|
<TableHead>服务端点</TableHead>
|
||||||
|
<TableHead>状态</TableHead>
|
||||||
|
<TableHead className="text-right">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{models.map((model) => (
|
||||||
|
<TableRow key={model.id} className="hover:bg-muted/50">
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold">
|
||||||
|
{model.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-foreground">
|
||||||
|
{model.name}
|
||||||
|
</div>
|
||||||
|
{model.tags.length > 0 && (
|
||||||
|
<div className="flex gap-1 mt-1">
|
||||||
|
{model.tags.slice(0, 2).map((tag, index) => (
|
||||||
|
<Badge key={index} variant="outline" className="text-xs">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{model.tags.length > 2 && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
+{model.tags.length - 2}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="font-medium">{getModelTypeLabel(model.type)}</div>
|
||||||
|
<div className="text-muted-foreground">TensorFlow</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline">
|
||||||
|
{model.version}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-muted-foreground w-12">准确率:</span>
|
||||||
|
<span className={`font-medium ${
|
||||||
|
model.metrics.accuracy >= 95 ? 'text-green-600 dark:text-green-400' :
|
||||||
|
model.metrics.accuracy >= 85 ? 'text-yellow-600 dark:text-yellow-400' :
|
||||||
|
'text-red-600 dark:text-red-400'
|
||||||
|
}`}>
|
||||||
|
{model.metrics.accuracy.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-muted-foreground w-12">响应:</span>
|
||||||
|
<span className="text-muted-foreground">{model.metrics.avg_response_time}ms</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-muted-foreground w-12">QPS:</span>
|
||||||
|
<span className="font-medium">{model.metrics.qps.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-muted-foreground w-12">调用:</span>
|
||||||
|
<span className="text-muted-foreground">{model.metrics.total_calls.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{getVisibilityBadge(model.visibility)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="max-w-32">
|
||||||
|
<div className="text-xs font-mono truncate">
|
||||||
|
{model.apiEndpoint}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => window.open(model.apiEndpoint, '_blank')}
|
||||||
|
title="访问API"
|
||||||
|
className="h-6 px-2 mt-1"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{getStatusBadge(model.status)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
{/* 停止/启动 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onToggle(model.id)}
|
||||||
|
title={model.status === 'active' ? '停止服务' : '启动服务'}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Power className={`h-4 w-4 ${
|
||||||
|
model.status === 'active' ? 'text-orange-500' : 'text-green-500'
|
||||||
|
}`} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 查看详情 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onView(model)}
|
||||||
|
title="查看详情"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4 text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 编辑 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onEdit(model)}
|
||||||
|
title="编辑模型"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4 text-yellow-500" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 权限管理 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPermission(model)}
|
||||||
|
title="权限管理"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Users className="h-4 w-4 text-purple-500" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 性能调优 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPerformance(model)}
|
||||||
|
title="性能调优"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Zap className="h-4 w-4 text-cyan-500" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 重新部署 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onRedeploy(model)}
|
||||||
|
title="重新部署"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-4 w-4 text-indigo-500" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 删除 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onDelete(model)}
|
||||||
|
title="删除模型"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-border bg-muted/30">
|
||||||
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
共 {models.length} 个模型服务 |
|
||||||
|
运行中: {models.filter(m => m.status === 'active').length} |
|
||||||
|
测试中: {models.filter(m => m.status === 'testing').length}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
总调用次数: {models.reduce((sum, m) => sum + m.metrics.total_calls, 0).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,465 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 性能调优对话框组件 - 模型性能参数优化
|
||||||
|
* 功能:当前性能指标、负载均衡策略、缓存优化配置
|
||||||
|
* 路径:/ai-crop-model/model-integration/management
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import {
|
||||||
|
Zap,
|
||||||
|
Activity,
|
||||||
|
Server,
|
||||||
|
Database,
|
||||||
|
Gauge,
|
||||||
|
TrendingUp,
|
||||||
|
Settings
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface PerformanceDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
model: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PerformanceDialog({ open, onOpenChange, model }: PerformanceDialogProps) {
|
||||||
|
const [performanceSettings, setPerformanceSettings] = useState({
|
||||||
|
// 基础性能配置
|
||||||
|
concurrency: model?.concurrency || 100,
|
||||||
|
timeout: model?.timeout || 30000,
|
||||||
|
retryCount: model?.retryCount || 3,
|
||||||
|
|
||||||
|
// 负载均衡策略
|
||||||
|
loadBalancing: {
|
||||||
|
strategy: 'round_robin',
|
||||||
|
healthCheck: true,
|
||||||
|
healthCheckInterval: 30,
|
||||||
|
failoverEnabled: true,
|
||||||
|
maxFailures: 3
|
||||||
|
},
|
||||||
|
|
||||||
|
// 缓存配置
|
||||||
|
caching: {
|
||||||
|
enabled: true,
|
||||||
|
ttl: 300,
|
||||||
|
maxSize: 1000,
|
||||||
|
strategy: 'lru'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 资源限制
|
||||||
|
resources: {
|
||||||
|
maxMemory: '2GB',
|
||||||
|
maxCpu: '50%',
|
||||||
|
maxGpu: '1'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBasicSettingChange = (field: string, value: any) => {
|
||||||
|
setPerformanceSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoadBalancingChange = (field: string, value: any) => {
|
||||||
|
setPerformanceSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
loadBalancing: {
|
||||||
|
...prev.loadBalancing,
|
||||||
|
[field]: value
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCachingChange = (field: string, value: any) => {
|
||||||
|
setPerformanceSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
caching: {
|
||||||
|
...prev.caching,
|
||||||
|
[field]: value
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResourcesChange = (field: string, value: any) => {
|
||||||
|
setPerformanceSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
resources: {
|
||||||
|
...prev.resources,
|
||||||
|
[field]: value
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const savePerformanceSettings = () => {
|
||||||
|
toast.success('性能配置已保存');
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟当前性能指标
|
||||||
|
const currentMetrics = {
|
||||||
|
cpuUsage: 65,
|
||||||
|
memoryUsage: 78,
|
||||||
|
gpuUsage: 45,
|
||||||
|
avgResponseTime: model?.metrics?.avg_response_time || 245,
|
||||||
|
qps: model?.metrics?.qps || 12.5,
|
||||||
|
cacheHitRate: 82,
|
||||||
|
errorRate: 1.5
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>性能调优</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
优化模型 <strong>{model?.name}</strong> 的性能参数和资源配置
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
{/* 当前性能指标 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Gauge className="w-5 h-5 text-blue-600" />
|
||||||
|
<Label className="text-base font-medium">当前性能指标</Label>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<Activity className="w-4 h-4 text-blue-600" />
|
||||||
|
<span className="text-sm font-medium">CPU使用率</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold">{currentMetrics.cpuUsage}%</div>
|
||||||
|
<Progress value={currentMetrics.cpuUsage} className="mt-2" />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<Database className="w-4 h-4 text-green-600" />
|
||||||
|
<span className="text-sm font-medium">内存使用率</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold">{currentMetrics.memoryUsage}%</div>
|
||||||
|
<Progress value={currentMetrics.memoryUsage} className="mt-2" />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<Zap className="w-4 h-4 text-purple-600" />
|
||||||
|
<span className="text-sm font-medium">响应时间</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold">{currentMetrics.avgResponseTime}ms</div>
|
||||||
|
<Badge
|
||||||
|
className={
|
||||||
|
currentMetrics.avgResponseTime < 200
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: currentMetrics.avgResponseTime < 500
|
||||||
|
? 'bg-yellow-100 text-yellow-800'
|
||||||
|
: 'bg-red-100 text-red-800'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{currentMetrics.avgResponseTime < 200 ? '优秀' : currentMetrics.avgResponseTime < 500 ? '良好' : '需优化'}
|
||||||
|
</Badge>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<TrendingUp className="w-4 h-4 text-orange-600" />
|
||||||
|
<span className="text-sm font-medium">QPS</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold">{currentMetrics.qps}</div>
|
||||||
|
<Badge className="bg-blue-100 text-blue-800">
|
||||||
|
当前
|
||||||
|
</Badge>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 基础性能配置 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Settings className="w-5 h-5 text-green-600" />
|
||||||
|
<Label className="text-base font-medium">基础性能配置</Label>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-4 p-4 border rounded-lg">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>并发数</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={performanceSettings.concurrency}
|
||||||
|
onChange={(e) => handleBasicSettingChange('concurrency', parseInt(e.target.value) || 0)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>超时时间 (ms)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={performanceSettings.timeout}
|
||||||
|
onChange={(e) => handleBasicSettingChange('timeout', parseInt(e.target.value) || 0)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>重试次数</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={performanceSettings.retryCount}
|
||||||
|
onChange={(e) => handleBasicSettingChange('retryCount', parseInt(e.target.value) || 0)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 负载均衡策略 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Server className="w-5 h-5 text-orange-600" />
|
||||||
|
<Label className="text-base font-medium">负载均衡策略</Label>
|
||||||
|
</div>
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>负载均衡算法</Label>
|
||||||
|
<Select
|
||||||
|
value={performanceSettings.loadBalancing.strategy}
|
||||||
|
onValueChange={(value) => handleLoadBalancingChange('strategy', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="round_robin">轮询 (Round Robin)</SelectItem>
|
||||||
|
<SelectItem value="least_connections">最少连接 (Least Connections)</SelectItem>
|
||||||
|
<SelectItem value="weighted_round_robin">加权轮询 (Weighted Round Robin)</SelectItem>
|
||||||
|
<SelectItem value="random">随机 (Random)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>健康检查间隔 (秒)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={performanceSettings.loadBalancing.healthCheckInterval}
|
||||||
|
onChange={(e) => handleLoadBalancingChange('healthCheckInterval', parseInt(e.target.value) || 0)}
|
||||||
|
disabled={!performanceSettings.loadBalancing.healthCheck}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">启用健康检查</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
定期检查服务健康状态
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={performanceSettings.loadBalancing.healthCheck}
|
||||||
|
onCheckedChange={(checked) => handleLoadBalancingChange('healthCheck', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">启用故障转移</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
服务失败时自动切换
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={performanceSettings.loadBalancing.failoverEnabled}
|
||||||
|
onCheckedChange={(checked) => handleLoadBalancingChange('failoverEnabled', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>最大失败次数</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={performanceSettings.loadBalancing.maxFailures}
|
||||||
|
onChange={(e) => handleLoadBalancingChange('maxFailures', parseInt(e.target.value) || 0)}
|
||||||
|
disabled={!performanceSettings.loadBalancing.failoverEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 缓存优化 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database className="w-5 h-5 text-purple-600" />
|
||||||
|
<Label className="text-base font-medium">缓存优化</Label>
|
||||||
|
</div>
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">启用缓存</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
缓存频繁请求的结果以提高响应速度
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={performanceSettings.caching.enabled}
|
||||||
|
onCheckedChange={(checked) => handleCachingChange('enabled', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{performanceSettings.caching.enabled && (
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>缓存时间 (秒)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={performanceSettings.caching.ttl}
|
||||||
|
onChange={(e) => handleCachingChange('ttl', parseInt(e.target.value) || 0)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>最大缓存条目</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={performanceSettings.caching.maxSize}
|
||||||
|
onChange={(e) => handleCachingChange('maxSize', parseInt(e.target.value) || 0)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>缓存策略</Label>
|
||||||
|
<Select
|
||||||
|
value={performanceSettings.caching.strategy}
|
||||||
|
onValueChange={(value) => handleCachingChange('strategy', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="lru">最近最少使用 (LRU)</SelectItem>
|
||||||
|
<SelectItem value="lfu">最少使用频率 (LFU)</SelectItem>
|
||||||
|
<SelectItem value="fifo">先进先出 (FIFO)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<Card className="p-3">
|
||||||
|
<div className="text-sm text-muted-foreground">当前缓存命中率</div>
|
||||||
|
<div className="text-lg font-bold text-green-600">{currentMetrics.cacheHitRate}%</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-3">
|
||||||
|
<div className="text-sm text-muted-foreground">当前错误率</div>
|
||||||
|
<div className="text-lg font-bold text-red-600">{currentMetrics.errorRate}%</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-3">
|
||||||
|
<div className="text-sm text-muted-foreground">GPU使用率</div>
|
||||||
|
<div className="text-lg font-bold text-purple-600">{currentMetrics.gpuUsage}%</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 资源限制 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Activity className="w-5 h-5 text-red-600" />
|
||||||
|
<Label className="text-base font-medium">资源限制</Label>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-4 p-4 border rounded-lg">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>最大内存</Label>
|
||||||
|
<Select
|
||||||
|
value={performanceSettings.resources.maxMemory}
|
||||||
|
onValueChange={(value) => handleResourcesChange('maxMemory', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1GB">1GB</SelectItem>
|
||||||
|
<SelectItem value="2GB">2GB</SelectItem>
|
||||||
|
<SelectItem value="4GB">4GB</SelectItem>
|
||||||
|
<SelectItem value="8GB">8GB</SelectItem>
|
||||||
|
<SelectItem value="16GB">16GB</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>最大CPU</Label>
|
||||||
|
<Select
|
||||||
|
value={performanceSettings.resources.maxCpu}
|
||||||
|
onValueChange={(value) => handleResourcesChange('maxCpu', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="25%">25%</SelectItem>
|
||||||
|
<SelectItem value="50%">50%</SelectItem>
|
||||||
|
<SelectItem value="75%">75%</SelectItem>
|
||||||
|
<SelectItem value="100%">100%</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>最大GPU</Label>
|
||||||
|
<Select
|
||||||
|
value={performanceSettings.resources.maxGpu}
|
||||||
|
onValueChange={(value) => handleResourcesChange('maxGpu', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0">不使用</SelectItem>
|
||||||
|
<SelectItem value="1">1个</SelectItem>
|
||||||
|
<SelectItem value="2">2个</SelectItem>
|
||||||
|
<SelectItem value="4">4个</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={savePerformanceSettings}>
|
||||||
|
保存配置
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 权限管理对话框组件 - 模型访问权限设置
|
||||||
|
* 功能:访问级别、API限流配置、IP白名单管理
|
||||||
|
* 路径:/ai-crop-model/model-integration/management
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Plus, X, Shield, Clock, Globe } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface PermissionDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
model: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PermissionDialog({ open, onOpenChange, model }: PermissionDialogProps) {
|
||||||
|
const [accessLevel, setAccessLevel] = useState(model?.visibility || 'private');
|
||||||
|
const [ipWhitelist, setIpWhitelist] = useState<string[]>(['192.168.1.100', '10.0.0.50']);
|
||||||
|
const [newIp, setNewIp] = useState('');
|
||||||
|
const [apiRateLimit, setApiRateLimit] = useState({
|
||||||
|
enabled: true,
|
||||||
|
requestsPerMinute: 100,
|
||||||
|
requestsPerHour: 1000,
|
||||||
|
requestsPerDay: 10000
|
||||||
|
});
|
||||||
|
const [authRequired, setAuthRequired] = useState(true);
|
||||||
|
const [apiKeyRequired, setApiKeyRequired] = useState(true);
|
||||||
|
|
||||||
|
const addIpToWhitelist = () => {
|
||||||
|
if (newIp && !ipWhitelist.includes(newIp)) {
|
||||||
|
setIpWhitelist([...ipWhitelist, newIp]);
|
||||||
|
setNewIp('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeIpFromWhitelist = (ip: string) => {
|
||||||
|
setIpWhitelist(ipWhitelist.filter(ipAddr => ipAddr !== ip));
|
||||||
|
};
|
||||||
|
|
||||||
|
const savePermissions = () => {
|
||||||
|
toast.success('权限设置已保存');
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>权限管理</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
设置模型 <strong>{model?.name}</strong> 的访问权限和安全配置
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
{/* 访问级别 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Globe className="w-5 h-5 text-blue-600" />
|
||||||
|
<Label className="text-base font-medium">访问级别</Label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/50">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="access"
|
||||||
|
checked={accessLevel === 'public'}
|
||||||
|
onChange={() => setAccessLevel('public')}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">公开访问</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
所有人都可以访问此模型API
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-green-100 text-green-800">公开</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/50">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="access"
|
||||||
|
checked={accessLevel === 'team'}
|
||||||
|
onChange={() => setAccessLevel('team')}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">团队访问</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
仅限团队成员可以访问此模型
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-blue-100 text-blue-800">团队</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/50">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="access"
|
||||||
|
checked={accessLevel === 'private'}
|
||||||
|
onChange={() => setAccessLevel('private')}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">私有访问</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
仅您可以访问此模型
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-red-100 text-red-800">私有</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API限流配置 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="w-5 h-5 text-orange-600" />
|
||||||
|
<Label className="text-base font-medium">API限流配置</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">启用API限流</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
限制API调用频率,防止滥用
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={apiRateLimit.enabled}
|
||||||
|
onCheckedChange={(checked) => setApiRateLimit(prev => ({ ...prev, enabled: checked }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{apiRateLimit.enabled && (
|
||||||
|
<div className="grid grid-cols-3 gap-4 p-4 border rounded-lg bg-muted/30">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>每分钟限制</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={apiRateLimit.requestsPerMinute}
|
||||||
|
onChange={(e) => setApiRateLimit(prev => ({
|
||||||
|
...prev,
|
||||||
|
requestsPerMinute: parseInt(e.target.value) || 0
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>每小时限制</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={apiRateLimit.requestsPerHour}
|
||||||
|
onChange={(e) => setApiRateLimit(prev => ({
|
||||||
|
...prev,
|
||||||
|
requestsPerHour: parseInt(e.target.value) || 0
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>每天限制</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={apiRateLimit.requestsPerDay}
|
||||||
|
onChange={(e) => setApiRateLimit(prev => ({
|
||||||
|
...prev,
|
||||||
|
requestsPerDay: parseInt(e.target.value) || 0
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* IP白名单 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Shield className="w-5 h-5 text-green-600" />
|
||||||
|
<Label className="text-base font-medium">IP白名单</Label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Input
|
||||||
|
placeholder="输入IP地址添加到白名单"
|
||||||
|
value={newIp}
|
||||||
|
onChange={(e) => setNewIp(e.target.value)}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button onClick={addIpToWhitelist} size="sm">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{ipWhitelist.map((ip, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-3 bg-muted rounded">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Globe className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<code className="text-sm font-mono">{ip}</code>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeIpFromWhitelist(ip)}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{ipWhitelist.length === 0 && (
|
||||||
|
<div className="text-center text-muted-foreground py-4">
|
||||||
|
暂无IP白名单配置
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 认证设置 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Shield className="w-5 h-5 text-purple-600" />
|
||||||
|
<Label className="text-base font-medium">认证设置</Label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">需要身份验证</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
访问API前需要用户登录验证
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={authRequired}
|
||||||
|
onCheckedChange={setAuthRequired}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">需要API密钥</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
API调用需要提供有效的API密钥
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={apiKeyRequired}
|
||||||
|
onCheckedChange={setApiKeyRequired}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={savePermissions}>
|
||||||
|
保存设置
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 重新部署对话框组件 - 模型服务重新部署
|
||||||
|
* 功能:部署确认、版本选择、部署进度监控
|
||||||
|
* 路径:/ai-crop-model/model-integration/management
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { RotateCcw, AlertTriangle, CheckCircle, Clock } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface RedeployDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
model: any;
|
||||||
|
dispatch: React.Dispatch<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RedeployDialog({ open, onOpenChange, model, dispatch }: RedeployDialogProps) {
|
||||||
|
const [deployVersion, setDeployVersion] = useState(model?.version || 'latest');
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
const [deployProgress, setDeployProgress] = useState(0);
|
||||||
|
const [deployStatus, setDeployStatus] = useState<'idle' | 'deploying' | 'success' | 'error'>('idle');
|
||||||
|
|
||||||
|
const handleRedeploy = async () => {
|
||||||
|
setIsDeploying(true);
|
||||||
|
setDeployStatus('deploying');
|
||||||
|
setDeployProgress(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟部署过程
|
||||||
|
const steps = [
|
||||||
|
{ progress: 20, message: '停止当前服务...' },
|
||||||
|
{ progress: 40, message: '备份当前版本...' },
|
||||||
|
{ progress: 60, message: '加载新版本模型...' },
|
||||||
|
{ progress: 80, message: '运行健康检查...' },
|
||||||
|
{ progress: 95, message: '重启服务...' },
|
||||||
|
{ progress: 100, message: '部署完成' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const step of steps) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
setDeployProgress(step.progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeployStatus('success');
|
||||||
|
toast.success('模型重新部署成功!');
|
||||||
|
|
||||||
|
// 更新模型状态
|
||||||
|
setTimeout(() => {
|
||||||
|
onOpenChange(false);
|
||||||
|
setDeployStatus('idle');
|
||||||
|
setDeployProgress(0);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
setDeployStatus('error');
|
||||||
|
toast.error('部署失败,请重试');
|
||||||
|
console.error('Deploy error:', error);
|
||||||
|
} finally {
|
||||||
|
setIsDeploying(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const availableVersions = [
|
||||||
|
{ version: 'v2.1.1', status: 'stable', date: '2024-03-21' },
|
||||||
|
{ version: 'v2.1.0', status: 'current', date: '2024-03-20' },
|
||||||
|
{ version: 'v2.0.9', status: 'stable', date: '2024-03-15' },
|
||||||
|
{ version: 'v2.1.2-beta', status: 'beta', date: '2024-03-22' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>重新部署</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
确认要重新部署模型 <strong>{model?.name}</strong> 吗?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
{deployStatus === 'idle' && (
|
||||||
|
<>
|
||||||
|
<Alert>
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
重新部署将会暂时停止服务,更新完成后自动重启。
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>选择部署版本</Label>
|
||||||
|
<Select value={deployVersion} onValueChange={setDeployVersion}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableVersions.map((version) => (
|
||||||
|
<SelectItem key={version.version} value={version.version}>
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
<span>{version.version}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
version.status === 'current'
|
||||||
|
? 'default'
|
||||||
|
: version.status === 'stable'
|
||||||
|
? 'secondary'
|
||||||
|
: 'outline'
|
||||||
|
}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{version.status === 'current' ? '当前' :
|
||||||
|
version.status === 'stable' ? '稳定' : '测试'}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{version.date}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<div className="font-medium mb-2">部署将执行以下操作:</div>
|
||||||
|
<ul className="space-y-1 ml-4">
|
||||||
|
<li>• 停止当前运行的模型服务</li>
|
||||||
|
<li>• 备份当前版本配置</li>
|
||||||
|
<li>• 部署新版本模型</li>
|
||||||
|
<li>• 运行健康检查和测试</li>
|
||||||
|
<li>• 重启服务并恢复调用</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{deployStatus === 'deploying' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RotateCcw className="w-4 h-4 animate-spin text-blue-600" />
|
||||||
|
<span className="font-medium">正在部署...</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={deployProgress} className="w-full" />
|
||||||
|
<div className="text-sm text-muted-foreground text-center">
|
||||||
|
{deployProgress < 20 && '准备部署环境...'}
|
||||||
|
{deployProgress >= 20 && deployProgress < 40 && '停止当前服务...'}
|
||||||
|
{deployProgress >= 40 && deployProgress < 60 && '备份当前版本...'}
|
||||||
|
{deployProgress >= 60 && deployProgress < 80 && '加载新版本模型...'}
|
||||||
|
{deployProgress >= 80 && deployProgress < 95 && '运行健康检查...'}
|
||||||
|
{deployProgress >= 95 && '重启服务...'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{deployStatus === 'success' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2 text-green-600">
|
||||||
|
<CheckCircle className="w-5 h-5" />
|
||||||
|
<span className="font-medium">部署成功!</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
模型已成功重新部署到版本 {deployVersion},服务正在恢复中...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{deployStatus === 'error' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2 text-red-600">
|
||||||
|
<AlertTriangle className="w-5 h-5" />
|
||||||
|
<span className="font-medium">部署失败</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
部署过程中遇到错误,请检查日志并重试。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isDeploying}
|
||||||
|
>
|
||||||
|
{deployStatus === 'success' ? '完成' : '取消'}
|
||||||
|
</Button>
|
||||||
|
{deployStatus === 'idle' && (
|
||||||
|
<Button onClick={handleRedeploy} disabled={isDeploying}>
|
||||||
|
确认部署
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 模型集成管理状态管理 - 模型服务与参数集中管理
|
||||||
|
* 功能:模型状态管理、弹窗控制、数据持久化、筛选功能
|
||||||
|
* 路径:/ai-crop-model/model-integration/management
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,shadcn语义化样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ModelService, FilterOptions } from '../types';
|
||||||
|
|
||||||
|
export interface ModelIntegrationState {
|
||||||
|
// 模型服务数据
|
||||||
|
models: ModelService[];
|
||||||
|
|
||||||
|
// 筛选选项
|
||||||
|
filters: FilterOptions;
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
totalModels: number;
|
||||||
|
runningModels: number;
|
||||||
|
avgAccuracy: number;
|
||||||
|
qps: number;
|
||||||
|
|
||||||
|
// 弹窗状态
|
||||||
|
showAddModelDialog: boolean;
|
||||||
|
showConfigDialog: boolean;
|
||||||
|
showViewDialog: boolean;
|
||||||
|
showDeleteDialog: boolean;
|
||||||
|
showPermissionDialog: boolean;
|
||||||
|
showPerformanceDialog: boolean;
|
||||||
|
showRedeployDialog: boolean;
|
||||||
|
|
||||||
|
// 选中的模型
|
||||||
|
selectedModel: ModelService | null;
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModelIntegrationAction =
|
||||||
|
| { type: 'LOAD_DATA' }
|
||||||
|
| { type: 'SET_LOADING'; payload: boolean }
|
||||||
|
| { type: 'SET_MODELS'; payload: ModelService[] }
|
||||||
|
| { type: 'UPDATE_FILTERS'; payload: Partial<FilterOptions> }
|
||||||
|
| { type: 'ADD_MODEL'; payload: ModelService }
|
||||||
|
| { type: 'UPDATE_MODEL'; payload: ModelService }
|
||||||
|
| { type: 'DELETE_MODEL'; payload: string }
|
||||||
|
| { type: 'TOGGLE_MODEL_STATUS'; payload: string }
|
||||||
|
| { type: 'SET_SELECTED_MODEL'; payload: ModelService | null }
|
||||||
|
| { type: 'SHOW_ADD_MODEL_DIALOG' }
|
||||||
|
| { type: 'SHOW_CONFIG_DIALOG'; payload: ModelService }
|
||||||
|
| { type: 'SHOW_VIEW_DIALOG'; payload: ModelService }
|
||||||
|
| { type: 'SHOW_DELETE_DIALOG'; payload: ModelService }
|
||||||
|
| { type: 'SHOW_PERMISSION_DIALOG'; payload: ModelService }
|
||||||
|
| { type: 'SHOW_PERFORMANCE_DIALOG'; payload: ModelService }
|
||||||
|
| { type: 'SHOW_REDEPLOY_DIALOG'; payload: ModelService }
|
||||||
|
| { type: 'SET_ADD_MODEL_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_CONFIG_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_VIEW_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_DELETE_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_PERMISSION_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_PERFORMANCE_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_REDEPLOY_DIALOG'; payload: boolean };
|
||||||
|
|
||||||
|
const generateMockModels = (): ModelService[] => [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: '病虫害识别模型',
|
||||||
|
type: 'pest_identification',
|
||||||
|
version: 'v2.1.0',
|
||||||
|
status: 'active',
|
||||||
|
description: '基于深度学习的农作物病虫害图像识别模型,支持52种常见病虫害识别',
|
||||||
|
apiEndpoint: 'https://api.smart-crop.com/v2/models/pest-detection',
|
||||||
|
parameters: {
|
||||||
|
input_size: [224, 224],
|
||||||
|
confidence_threshold: 0.85,
|
||||||
|
max_detections: 10
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
total_calls: 15420,
|
||||||
|
avg_response_time: 245,
|
||||||
|
success_rate: 98.5,
|
||||||
|
accuracy: 95.2,
|
||||||
|
last_called: new Date(Date.now() - 1000 * 60 * 5).toISOString(),
|
||||||
|
qps: 12.5
|
||||||
|
},
|
||||||
|
tags: ['图像识别', '病虫害', '深度学习'],
|
||||||
|
visibility: 'public',
|
||||||
|
team: 'AI算法团队',
|
||||||
|
concurrency: 100,
|
||||||
|
timeout: 30000,
|
||||||
|
retryCount: 3,
|
||||||
|
createdAt: '2024-03-15T08:00:00Z',
|
||||||
|
updatedAt: '2024-03-20T14:30:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: '产量预测模型',
|
||||||
|
type: 'yield_prediction',
|
||||||
|
version: 'v1.3.2',
|
||||||
|
status: 'active',
|
||||||
|
description: '基于历史数据和环境因素的农作物产量预测模型',
|
||||||
|
apiEndpoint: 'https://api.smart-crop.com/v2/models/yield-prediction',
|
||||||
|
parameters: {
|
||||||
|
weather_features: true,
|
||||||
|
soil_features: true,
|
||||||
|
historical_days: 30,
|
||||||
|
prediction_horizon: 90
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
total_calls: 8960,
|
||||||
|
avg_response_time: 890,
|
||||||
|
success_rate: 99.1,
|
||||||
|
accuracy: 88.7,
|
||||||
|
last_called: new Date(Date.now() - 1000 * 60 * 15).toISOString(),
|
||||||
|
qps: 8.2
|
||||||
|
},
|
||||||
|
tags: ['预测分析', '产量', '机器学习'],
|
||||||
|
visibility: 'team',
|
||||||
|
team: '数据分析团队',
|
||||||
|
concurrency: 50,
|
||||||
|
timeout: 60000,
|
||||||
|
retryCount: 2,
|
||||||
|
createdAt: '2024-02-20T10:00:00Z',
|
||||||
|
updatedAt: '2024-03-18T16:45:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: '生长状态识别模型',
|
||||||
|
type: 'growth_status',
|
||||||
|
version: 'v1.2.0',
|
||||||
|
status: 'active',
|
||||||
|
description: '基于作物图像的生长阶段自动识别模型',
|
||||||
|
apiEndpoint: 'https://api.smart-crop.com/v2/models/growth-stage',
|
||||||
|
parameters: {
|
||||||
|
input_size: [256, 256],
|
||||||
|
confidence_threshold: 0.75,
|
||||||
|
growth_stages: ['seedling', 'vegetative', 'flowering', 'fruiting', 'mature']
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
total_calls: 2150,
|
||||||
|
avg_response_time: 445,
|
||||||
|
success_rate: 94.2,
|
||||||
|
accuracy: 89.3,
|
||||||
|
last_called: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
|
||||||
|
qps: 4.2
|
||||||
|
},
|
||||||
|
tags: ['图像识别', '生长阶段', 'AI分析'],
|
||||||
|
visibility: 'public',
|
||||||
|
team: 'AI算法团队',
|
||||||
|
concurrency: 20,
|
||||||
|
timeout: 25000,
|
||||||
|
retryCount: 1,
|
||||||
|
createdAt: '2024-03-10T15:00:00Z',
|
||||||
|
updatedAt: '2024-03-22T10:30:00Z'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const calculateStatistics = (models: ModelService[]) => {
|
||||||
|
const totalModels = models.length;
|
||||||
|
const runningModels = models.filter(m => m.status === 'active').length;
|
||||||
|
|
||||||
|
const avgAccuracy = models.length > 0
|
||||||
|
? (models.reduce((sum, model) => sum + model.metrics.accuracy, 0) / models.length).toFixed(1)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const qps = models.length > 0
|
||||||
|
? models.reduce((sum, model) => sum + model.metrics.qps, 0).toFixed(1)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalModels,
|
||||||
|
runningModels,
|
||||||
|
avgAccuracy: parseFloat(avgAccuracy),
|
||||||
|
qps: parseFloat(qps)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialState: ModelIntegrationState = {
|
||||||
|
models: [],
|
||||||
|
filters: {
|
||||||
|
searchTerm: '',
|
||||||
|
typeFilter: 'all',
|
||||||
|
statusFilter: 'all'
|
||||||
|
},
|
||||||
|
totalModels: 0,
|
||||||
|
runningModels: 0,
|
||||||
|
avgAccuracy: 0,
|
||||||
|
qps: 0,
|
||||||
|
showAddModelDialog: false,
|
||||||
|
showConfigDialog: false,
|
||||||
|
showViewDialog: false,
|
||||||
|
showDeleteDialog: false,
|
||||||
|
showPermissionDialog: false,
|
||||||
|
showPerformanceDialog: false,
|
||||||
|
showRedeployDialog: false,
|
||||||
|
selectedModel: null,
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export const modelIntegrationReducer = (
|
||||||
|
state: ModelIntegrationState,
|
||||||
|
action: ModelIntegrationAction
|
||||||
|
): ModelIntegrationState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'LOAD_DATA':
|
||||||
|
const models = generateMockModels();
|
||||||
|
const statistics = calculateStatistics(models);
|
||||||
|
|
||||||
|
// 持久化到localStorage
|
||||||
|
localStorage.setItem('smart_crop_model_integration_models', JSON.stringify(models));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
models,
|
||||||
|
...statistics,
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_LOADING':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_MODELS':
|
||||||
|
localStorage.setItem('smart_crop_model_integration_models', JSON.stringify(action.payload));
|
||||||
|
const updatedStatistics = calculateStatistics(action.payload);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
models: action.payload,
|
||||||
|
...updatedStatistics
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_FILTERS':
|
||||||
|
const newFilters = { ...state.filters, ...action.payload };
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filters: newFilters
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'ADD_MODEL':
|
||||||
|
const newModels = [...state.models, action.payload];
|
||||||
|
localStorage.setItem('smart_crop_model_integration_models', JSON.stringify(newModels));
|
||||||
|
const addStatistics = calculateStatistics(newModels);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
models: newModels,
|
||||||
|
...addStatistics
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_MODEL':
|
||||||
|
const updatedModels = state.models.map(model =>
|
||||||
|
model.id === action.payload.id ? { ...action.payload, updatedAt: new Date().toISOString() } : model
|
||||||
|
);
|
||||||
|
localStorage.setItem('smart_crop_model_integration_models', JSON.stringify(updatedModels));
|
||||||
|
const updateStatistics = calculateStatistics(updatedModels);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
models: updatedModels,
|
||||||
|
...updateStatistics
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'DELETE_MODEL':
|
||||||
|
const filteredModels = state.models.filter(model => model.id !== action.payload);
|
||||||
|
localStorage.setItem('smart_crop_model_integration_models', JSON.stringify(filteredModels));
|
||||||
|
const deleteStatistics = calculateStatistics(filteredModels);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
models: filteredModels,
|
||||||
|
...deleteStatistics
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'TOGGLE_MODEL_STATUS':
|
||||||
|
const toggledModels = state.models.map(model =>
|
||||||
|
model.id === action.payload
|
||||||
|
? {
|
||||||
|
...model,
|
||||||
|
status: model.status === 'active' ? 'inactive' : 'active' as 'active' | 'inactive',
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
: model
|
||||||
|
);
|
||||||
|
localStorage.setItem('smart_crop_model_integration_models', JSON.stringify(toggledModels));
|
||||||
|
const toggleStatistics = calculateStatistics(toggledModels);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
models: toggledModels,
|
||||||
|
...toggleStatistics
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_SELECTED_MODEL':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedModel: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_ADD_MODEL_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showAddModelDialog: true
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_CONFIG_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showConfigDialog: true,
|
||||||
|
selectedModel: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_VIEW_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showViewDialog: true,
|
||||||
|
selectedModel: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_DELETE_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showDeleteDialog: true,
|
||||||
|
selectedModel: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_PERMISSION_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showPermissionDialog: true,
|
||||||
|
selectedModel: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_PERFORMANCE_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showPerformanceDialog: true,
|
||||||
|
selectedModel: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SHOW_REDEPLOY_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showRedeployDialog: true,
|
||||||
|
selectedModel: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_ADD_MODEL_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showAddModelDialog: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_CONFIG_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showConfigDialog: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_VIEW_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showViewDialog: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_DELETE_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showDeleteDialog: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_PERMISSION_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showPermissionDialog: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_PERFORMANCE_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showPerformanceDialog: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_REDEPLOY_DIALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showRedeployDialog: action.payload
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,18 +1,356 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 模型集成管理页面 - AI模型服务与参数管理平台
|
||||||
|
* 功能:模型服务列表管理、统计卡片展示、搜索筛选功能、模型操作管理
|
||||||
|
* 路径:/ai-crop-model/model-integration/management
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,shadcn语义化样式
|
||||||
|
*/
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useReducer, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Brain,
|
||||||
|
PlayCircle,
|
||||||
|
Target,
|
||||||
|
Zap,
|
||||||
|
Search
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { ModelServiceList } from './components/ModelServiceList';
|
||||||
|
import { ModelAnalytics } from './components/ModelAnalytics';
|
||||||
|
import { AddModelDialog } from './components/AddModelDialog';
|
||||||
|
import { ModelConfigDialog } from './components/ModelConfigDialog';
|
||||||
|
import { DeleteModelDialog } from './components/DeleteModelDialog';
|
||||||
|
import { PermissionDialog } from './components/PermissionDialog';
|
||||||
|
import { PerformanceDialog } from './components/PerformanceDialog';
|
||||||
|
import { RedeployDialog } from './components/RedeployDialog';
|
||||||
|
import { modelIntegrationReducer, initialState } from './components/modelIntegrationReducer';
|
||||||
|
import { ModelType, ModelStatus } from './types';
|
||||||
|
|
||||||
|
export default function ModelIntegrationPage() {
|
||||||
|
const [state, dispatch] = useReducer(modelIntegrationReducer, initialState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({ type: 'LOAD_DATA' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddModel = () => {
|
||||||
|
dispatch({ type: 'SHOW_ADD_MODEL_DIALOG' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditModel = (model: any) => {
|
||||||
|
dispatch({ type: 'SHOW_CONFIG_DIALOG', payload: model });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewModel = (model: any) => {
|
||||||
|
dispatch({ type: 'SHOW_VIEW_DIALOG', payload: model });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteModel = (model: any) => {
|
||||||
|
dispatch({ type: 'SHOW_DELETE_DIALOG', payload: model });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleModel = (modelId: string) => {
|
||||||
|
dispatch({ type: 'TOGGLE_MODEL_STATUS', payload: modelId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePermission = (model: any) => {
|
||||||
|
dispatch({ type: 'SHOW_PERMISSION_DIALOG', payload: model });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePerformance = (model: any) => {
|
||||||
|
dispatch({ type: 'SHOW_PERFORMANCE_DIALOG', payload: model });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRedeploy = (model: any) => {
|
||||||
|
dispatch({ type: 'SHOW_REDEPLOY_DIALOG', payload: model });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = (value: string) => {
|
||||||
|
dispatch({ type: 'UPDATE_FILTERS', payload: { searchTerm: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTypeFilterChange = (value: string) => {
|
||||||
|
dispatch({ type: 'UPDATE_FILTERS', payload: { typeFilter: value as ModelType | 'all' } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusFilterChange = (value: string) => {
|
||||||
|
dispatch({ type: 'UPDATE_FILTERS', payload: { statusFilter: value as ModelStatus | 'all' } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 过滤模型
|
||||||
|
const filteredModels = state.models.filter(model => {
|
||||||
|
const matchesSearch = model.name.toLowerCase().includes(state.filters.searchTerm.toLowerCase()) ||
|
||||||
|
model.description.toLowerCase().includes(state.filters.searchTerm.toLowerCase());
|
||||||
|
const matchesType = state.filters.typeFilter === 'all' || model.type === state.filters.typeFilter;
|
||||||
|
const matchesStatus = state.filters.statusFilter === 'all' || model.status === state.filters.statusFilter;
|
||||||
|
|
||||||
|
return matchesSearch && matchesType && matchesStatus;
|
||||||
|
});
|
||||||
|
|
||||||
export default function ManagementPage() {
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="p-6">
|
{/* 页面头部 */}
|
||||||
<h2 className="text-xl font-semibold">模型管理</h2>
|
<div className="flex items-center justify-between">
|
||||||
<div className="p-3 bg-muted rounded-lg mt-3">
|
<div>
|
||||||
<p className="text-sm">
|
<h2 className="text-primary">模型管理</h2>
|
||||||
<strong>页面路径:</strong> /ai-crop-model/model-integration/management
|
<p className="text-muted-foreground">管理和监控AI模型服务,配置参数模板</p>
|
||||||
</p>
|
</div>
|
||||||
|
<Button onClick={handleAddModel}>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
新增模型
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 4个统计卡片 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-blue-600 dark:text-blue-400 font-medium">
|
||||||
|
模型总数
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-blue-700 dark:text-blue-300 mt-1">
|
||||||
|
{state.totalModels}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-blue-500 dark:text-blue-400 mt-1">
|
||||||
|
当前系统中的模型数量
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Brain className="w-8 h-8 text-blue-500 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-green-600 dark:text-green-400 font-medium">
|
||||||
|
运行中
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-green-700 dark:text-green-300 mt-1">
|
||||||
|
{state.runningModels}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-green-500 dark:text-green-400 mt-1">
|
||||||
|
正在提供服务的模型数量
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PlayCircle className="w-8 h-8 text-green-500 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-purple-600 dark:text-purple-400 font-medium">
|
||||||
|
平均准确率
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-purple-700 dark:text-purple-300 mt-1">
|
||||||
|
{state.avgAccuracy}%
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-purple-500 dark:text-purple-400 mt-1">
|
||||||
|
所有模型的平均准确率
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Target className="w-8 h-8 text-purple-500 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-orange-600 dark:text-orange-400 font-medium">
|
||||||
|
QPS
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-orange-700 dark:text-orange-300 mt-1">
|
||||||
|
{state.qps}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-orange-500 dark:text-orange-400 mt-1">
|
||||||
|
每秒查询请求数
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Zap className="w-8 h-8 text-orange-500 dark:text-orange-400" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 模型服务管理功能介绍 */}
|
||||||
|
<Card className="p-6 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
|
||||||
|
<h3 className="text-lg font-semibold text-green-800 dark:text-green-200 mb-4">模型服务管理功能:</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 mt-2 flex-shrink-0"></div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-green-800 dark:text-green-200">统一管理</div>
|
||||||
|
<div className="text-sm text-green-600 dark:text-green-400">集中管理所有已注册的模型服务</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 mt-2 flex-shrink-0"></div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-green-800 dark:text-green-200">元信息维护</div>
|
||||||
|
<div className="text-sm text-green-600 dark:text-green-400">模型描述、标签、版本等信息管理</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 mt-2 flex-shrink-0"></div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-green-800 dark:text-green-200">服务配置</div>
|
||||||
|
<div className="text-sm text-green-600 dark:text-green-400">并发数、超时、重试等参数配置</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 mt-2 flex-shrink-0"></div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-green-800 dark:text-green-200">权限控制</div>
|
||||||
|
<div className="text-sm text-green-600 dark:text-green-400">公开/私有/团队共享访问权限设置</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 mt-2 flex-shrink-0"></div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-green-800 dark:text-green-200">负载均衡</div>
|
||||||
|
<div className="text-sm text-green-600 dark:text-green-400">自动负载均衡与弹性伸缩</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 mt-2 flex-shrink-0"></div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-green-800 dark:text-green-200">高可用</div>
|
||||||
|
<div className="text-sm text-green-600 dark:text-green-400">启停控制、性能监控、依赖管理</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 搜索和筛选区域 */}
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label htmlFor="search">搜索模型</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||||
|
<Input
|
||||||
|
id="search"
|
||||||
|
placeholder="搜索模型名称或描述..."
|
||||||
|
value={state.filters.searchTerm}
|
||||||
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full md:w-48">
|
||||||
|
<Label htmlFor="typeFilter">模型类型</Label>
|
||||||
|
<Select value={state.filters.typeFilter} onValueChange={handleTypeFilterChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部类型</SelectItem>
|
||||||
|
<SelectItem value="pest_identification">病虫害识别</SelectItem>
|
||||||
|
<SelectItem value="growth_status">生长状态</SelectItem>
|
||||||
|
<SelectItem value="yield_prediction">产量预测</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full md:w-48">
|
||||||
|
<Label htmlFor="statusFilter">运行状态</Label>
|
||||||
|
<Select value={state.filters.statusFilter} onValueChange={handleStatusFilterChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部状态</SelectItem>
|
||||||
|
<SelectItem value="active">运行中</SelectItem>
|
||||||
|
<SelectItem value="inactive">已停止</SelectItem>
|
||||||
|
<SelectItem value="testing">测试中</SelectItem>
|
||||||
|
<SelectItem value="error">错误</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 模型分析图表 */}
|
||||||
|
<ModelAnalytics models={state.models} />
|
||||||
|
|
||||||
|
{/* 模型服务列表 */}
|
||||||
|
<ModelServiceList
|
||||||
|
models={filteredModels}
|
||||||
|
onEdit={handleEditModel}
|
||||||
|
onView={handleViewModel}
|
||||||
|
onDelete={handleDeleteModel}
|
||||||
|
onToggle={handleToggleModel}
|
||||||
|
onPermission={handlePermission}
|
||||||
|
onPerformance={handlePerformance}
|
||||||
|
onRedeploy={handleRedeploy}
|
||||||
|
loading={state.loading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 新增模型对话框 */}
|
||||||
|
<AddModelDialog
|
||||||
|
open={state.showAddModelDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_ADD_MODEL_DIALOG', payload: open })}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 权限管理对话框 */}
|
||||||
|
<PermissionDialog
|
||||||
|
open={state.showPermissionDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_PERMISSION_DIALOG', payload: open })}
|
||||||
|
model={state.selectedModel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 性能调优对话框 */}
|
||||||
|
<PerformanceDialog
|
||||||
|
open={state.showPerformanceDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_PERFORMANCE_DIALOG', payload: open })}
|
||||||
|
model={state.selectedModel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 重新部署对话框 */}
|
||||||
|
<RedeployDialog
|
||||||
|
open={state.showRedeployDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_REDEPLOY_DIALOG', payload: open })}
|
||||||
|
model={state.selectedModel}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 模型配置对话框 */}
|
||||||
|
<ModelConfigDialog
|
||||||
|
open={state.showConfigDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_CONFIG_DIALOG', payload: open })}
|
||||||
|
model={state.selectedModel}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 模型查看对话框 */}
|
||||||
|
<ModelConfigDialog
|
||||||
|
open={state.showViewDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_VIEW_DIALOG', payload: open })}
|
||||||
|
model={state.selectedModel}
|
||||||
|
viewMode={true}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 删除确认对话框 */}
|
||||||
|
<DeleteModelDialog
|
||||||
|
open={state.showDeleteDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'SET_DELETE_DIALOG', payload: open })}
|
||||||
|
model={state.selectedModel}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 模型集成管理类型定义 - 模型服务与参数类型规范
|
||||||
|
* 功能:模型服务接口定义、数据结构规范、枚举类型定义
|
||||||
|
* 路径:/ai-crop-model/model-integration/management
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用TypeScript严格类型检查
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ModelType =
|
||||||
|
| 'image_classification'
|
||||||
|
| 'object_detection'
|
||||||
|
| 'regression'
|
||||||
|
| 'multiclass_classification'
|
||||||
|
| 'optimization'
|
||||||
|
| 'time_series'
|
||||||
|
| 'nlp'
|
||||||
|
| 'anomaly_detection';
|
||||||
|
|
||||||
|
export type ModelStatus = 'active' | 'inactive' | 'testing' | 'error';
|
||||||
|
|
||||||
|
export interface ModelMetrics {
|
||||||
|
total_calls: number;
|
||||||
|
avg_response_time: number;
|
||||||
|
success_rate: number;
|
||||||
|
accuracy: number; // 准确率
|
||||||
|
last_called: string;
|
||||||
|
qps: number; // 每秒查询数
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelService {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: ModelType;
|
||||||
|
version: string;
|
||||||
|
status: ModelStatus;
|
||||||
|
description: string;
|
||||||
|
apiEndpoint: string;
|
||||||
|
parameters: Record<string, any>;
|
||||||
|
metrics: ModelMetrics;
|
||||||
|
tags: string[];
|
||||||
|
visibility: 'public' | 'private' | 'team';
|
||||||
|
team: string;
|
||||||
|
concurrency: number;
|
||||||
|
timeout: number;
|
||||||
|
retryCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterOptions {
|
||||||
|
searchTerm: string;
|
||||||
|
typeFilter: ModelType | 'all';
|
||||||
|
statusFilter: ModelStatus | 'all';
|
||||||
|
}
|
||||||
@@ -177,7 +177,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
// 每 1 分钟刷新一次 token
|
// 每 1 分钟刷新一次 token
|
||||||
refreshTimerRef.current = setInterval(() => {
|
refreshTimerRef.current = setInterval(() => {
|
||||||
refreshAccessToken();
|
refreshAccessToken();
|
||||||
}, 5 * 1000); // 60 秒 = 1 分钟
|
}, 5 * 60 * 1000); // 60 秒 = 1 分钟
|
||||||
|
|
||||||
console.log('🕐 Token 自动刷新定时器已启动(每5分钟)');
|
console.log('🕐 Token 自动刷新定时器已启动(每5分钟)');
|
||||||
};
|
};
|
||||||
|
|||||||
31
crop-x/src/lib/clipboard.ts
Normal file
31
crop-x/src/lib/clipboard.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 复制文本到剪贴板
|
||||||
|
* @param text 要复制的文本
|
||||||
|
* @returns 是否复制成功
|
||||||
|
*/
|
||||||
|
export const copyToClipboard = async (text: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
// 使用现代 Clipboard API
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// 降级方案:使用 document.execCommand
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-999999px';
|
||||||
|
textArea.style.top = '-999999px';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
const successful = document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
return successful;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to copy text: ', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user