Files
ai-learning-platform/frontend/src/components/ui/model-selector.tsx
T
yuzhiran-dev 538de50bb1 注册支持用户名/手机号/邮箱 + 镜像站部署脚本 + AI 助手 Tool Calling 重构
- Prisma User 模型新增 username 字段(唯一索引)
- 注册先查重复再创建,返回友好中文提示(非 500)
- 登录支持用户名/手机号/邮箱三种方式
- 前端注册表单增加用户名输入框,预校验 2-20 位格式
- 新增 scripts/deploy.sh:一键构建并部署主站+镜像站+重启后端+重载 Nginx
- 镜像站 www.yuzhiran.com.cn Nginx 配置与主站同步
- AI 助手架构升级:用户端/管理后台均采用完整 Tool Calling 架构
- 新增 UserAiAssistantService(18 工具)+ AiAssistantController
- admin 助手新增 search + mark-all-notifications-read 工具
- 修复注册 500 错误:catch Prisma P2002 → BadRequestException
- Baidu Analytics Script 注入 root layout
2026-06-01 23:14:42 +08:00

70 lines
2.9 KiB
TypeScript
Executable File

'use client';
import { useState, useEffect } from 'react';
import { AVAILABLE_MODELS as FALLBACK_MODELS, type ModelOption } from '@/lib/models';
import { API_BASE } from '@/lib/config';
interface ModelSelectorProps {
value: string;
onChange: (id: string) => void;
className?: string;
}
export function ModelSelector({ value, onChange, className = '' }: ModelSelectorProps) {
const [open, setOpen] = useState(false);
const [models, setModels] = useState<ModelOption[]>(FALLBACK_MODELS);
useEffect(() => {
fetch(`${API_BASE}/public/models`)
.then(r => r.json())
.then(data => {
if (data.items?.length) {
setModels(data.items.map((m: any) => ({
id: m.id,
label: m.name,
provider: m.provider,
desc: m.description || '',
})));
}
})
.catch(() => {});
}, []);
const selected = models.find(m => m.id === value) || models[0];
return (
<div className={`relative ${className}`}>
<button onClick={() => setOpen(!open)}
className="flex items-center gap-2 px-3 py-2 border border-border rounded-lg bg-background hover:bg-accent transition-colors text-sm w-full text-left">
<span className="w-2 h-2 rounded-full bg-brand-600 shrink-0" />
<span className="text-foreground font-medium">{selected.label}</span>
<svg className={`w-3.5 h-3.5 ml-auto text-muted-foreground transition-transform ${open ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{open && (
<>
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
<div className="absolute right-0 top-full mt-1 z-20 w-64 bg-card border border-border rounded-xl shadow-lg overflow-hidden">
{models.map(m => (
<button key={m.id} onClick={() => { onChange(m.id); setOpen(false); }}
className={`w-full text-left px-4 py-3 hover:bg-accent transition-colors flex items-center gap-3 ${m.id === value ? 'bg-accent' : ''}`}>
<span className={`w-2 h-2 rounded-full shrink-0 ${m.id === value ? 'bg-brand-600' : 'bg-muted-foreground/30'}`} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-foreground">{m.label}</div>
<div className="text-xs text-muted-foreground">{m.desc}</div>
</div>
{m.id === value && (
<svg className="w-4 h-4 text-brand-600 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
</button>
))}
</div>
</>
)}
</div>
);
}