feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径

This commit is contained in:
yuzhiran-dev
2026-05-18 09:48:51 +08:00
commit 11bb86854c
277 changed files with 37755 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
'use client';
import { useEffect, useState } from 'react';
import { Skeleton } from '@/components/ui/skeleton';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api/v1';
interface AiModel {
id: number;
name: string;
provider: string;
description: string | null;
capabilities: string | null;
contextWindow: number | null;
maxTokens: number | null;
pricing: string | null;
isFree: boolean;
isFeatured: boolean;
icon: string | null;
}
export default function ModelsPage() {
const [models, setModels] = useState<AiModel[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`${API_BASE}/models`)
.then(r => r.json())
.then(setModels)
.catch(() => {})
.finally(() => setLoading(false));
}, []);
if (loading) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center mb-12">
<Skeleton className="h-9 w-48 mx-auto mb-3" />
<Skeleton className="h-5 w-96 mx-auto" />
</div>
<div className="space-y-3">
{[1,2,3,4,5].map(i => (
<Skeleton key={i} className="h-16 w-full rounded-xl" />
))}
</div>
</div>
);
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center mb-12">
<h1 className="text-3xl font-bold text-foreground">AI </h1>
<p className="mt-3 text-muted-foreground max-w-2xl mx-auto">
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-4 px-4 font-semibold text-foreground"></th>
<th className="text-left py-4 px-4 font-semibold text-foreground"></th>
<th className="text-left py-4 px-4 font-semibold text-foreground hidden md:table-cell"></th>
<th className="text-right py-4 px-4 font-semibold text-foreground hidden lg:table-cell"></th>
<th className="text-right py-4 px-4 font-semibold text-foreground hidden lg:table-cell"></th>
<th className="text-center py-4 px-4 font-semibold text-foreground hidden sm:table-cell"></th>
</tr>
</thead>
<tbody>
{models.map((model) => (
<tr key={model.id} className="border-b border-border hover:bg-accent/50 transition-colors">
<td className="py-4 px-4">
<div className="font-semibold text-foreground">{model.name}</div>
{model.isFree && (
<span className="inline-block mt-1 text-xs px-1.5 py-0.5 bg-green-100 text-green-700 rounded"></span>
)}
{model.isFeatured && !model.isFree && (
<span className="inline-block mt-1 text-xs px-1.5 py-0.5 bg-brand-100 text-brand-700 rounded"></span>
)}
</td>
<td className="py-4 px-4 text-muted-foreground">{model.provider}</td>
<td className="py-4 px-4 text-muted-foreground hidden md:table-cell max-w-xs">
<div className="flex flex-wrap gap-1">
{model.capabilities?.split(',').map(cap => (
<span key={cap} className="text-xs px-1.5 py-0.5 bg-muted text-muted-foreground rounded">{cap.trim()}</span>
))}
</div>
</td>
<td className="py-4 px-4 text-right text-muted-foreground hidden lg:table-cell">
{model.contextWindow ? `${(model.contextWindow / 1000).toFixed(0)}K` : '-'}
</td>
<td className="py-4 px-4 text-right text-muted-foreground hidden lg:table-cell">
{model.maxTokens ? `${(model.maxTokens / 1024).toFixed(0)}K` : '-'}
</td>
<td className="py-4 px-4 text-center hidden sm:table-cell">
<span className={`text-xs px-2 py-1 rounded ${model.isFree ? 'bg-green-100 text-green-700' : 'bg-orange-100 text-orange-700'}`}>
{model.isFree ? '免费' : model.pricing?.includes('免费') ? '免费/付费' : '付费'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{models.length > 0 && (
<div className="mt-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{models.filter(m => m.isFeatured).map((model) => (
<div key={`card-${model.id}`} className="bg-card rounded-xl border border-border p-6 hover:border-brand-200 transition-colors">
<div className="flex items-start justify-between mb-3">
<h3 className="font-semibold text-foreground">{model.name}</h3>
{model.isFree && <span className="text-xs px-1.5 py-0.5 bg-green-100 text-green-700 rounded"></span>}
</div>
<p className="text-sm text-muted-foreground mb-3">{model.provider}</p>
<p className="text-sm text-muted-foreground line-clamp-2">{model.description}</p>
<div className="mt-4 flex flex-wrap gap-1">
{model.capabilities?.split(',').slice(0, 4).map(cap => (
<span key={cap} className="text-xs px-1.5 py-0.5 bg-brand-50 text-brand-600 rounded">{cap.trim()}</span>
))}
</div>
<div className="mt-4 pt-4 border-t border-border grid grid-cols-2 gap-3 text-xs text-muted-foreground">
<div>
<span className="block text-muted-foreground"></span>
{model.contextWindow ? `${(model.contextWindow / 1000).toFixed(0)}K tokens` : '-'}
</div>
<div>
<span className="block text-muted-foreground"></span>
{model.maxTokens ? `${(model.maxTokens / 1024).toFixed(0)}K tokens` : '-'}
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}