Add community templates feature with tabs in Models page - includes predefined templates and like functionality
45e0f20
| import { useState, useEffect } from 'react' | |
| import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' | |
| import { Button } from '@/components/ui/button' | |
| import { Badge } from '@/components/ui/badge' | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' | |
| import { | |
| BookOpen, | |
| Brain, | |
| Zap, | |
| Download, | |
| Trash2, | |
| Loader2, | |
| Info, | |
| CheckCircle, | |
| Cloud, | |
| HardDrive, | |
| Bot, | |
| MessageSquare, | |
| Users, | |
| Star, | |
| Heart, | |
| Share, | |
| Copy | |
| } from 'lucide-react' | |
| interface ModelInfo { | |
| model_name: string | |
| name: string | |
| supports_thinking: boolean | |
| description: string | |
| size_gb: string | |
| is_loaded: boolean | |
| type: 'local' | 'api' | |
| } | |
| interface ModelsResponse { | |
| models: ModelInfo[] | |
| current_model: string | |
| } | |
| // Community template interface | |
| interface CommunityTemplate { | |
| id: string | |
| name: string | |
| description: string | |
| author: string | |
| category: string | |
| tags: string[] | |
| model: string | |
| systemPrompt: string | |
| temperature: number | |
| maxTokens: number | |
| likes: number | |
| downloads: number | |
| isOfficial: boolean | |
| createdAt: string | |
| } | |
| // Get predefined community templates | |
| function getCommunityTemplates(): CommunityTemplate[] { | |
| return [ | |
| { | |
| id: 'code-reviewer', | |
| name: 'Code Review Expert', | |
| description: 'Professional code reviewer that provides detailed analysis, suggests improvements, and identifies potential issues.', | |
| author: 'EdgeLLM Team', | |
| category: 'coding', | |
| tags: ['code review', 'programming', 'best practices'], | |
| model: 'Qwen/Qwen3-30B-A3B', | |
| systemPrompt: 'You are a senior software engineer specializing in code review. Analyze the provided code for:\n\n1. **Code Quality**: Structure, readability, maintainability\n2. **Best Practices**: Following language conventions and patterns\n3. **Performance**: Potential optimizations and bottlenecks\n4. **Security**: Common vulnerabilities and security issues\n5. **Testing**: Testability and edge cases\n\nProvide constructive feedback with specific examples and actionable suggestions.', | |
| temperature: 0.3, | |
| maxTokens: 1500, | |
| likes: 245, | |
| downloads: 1200, | |
| isOfficial: true, | |
| createdAt: '2024-01-15' | |
| }, | |
| { | |
| id: 'writing-tutor', | |
| name: 'Academic Writing Tutor', | |
| description: 'Helps improve academic writing with structure suggestions, grammar corrections, and clarity enhancements.', | |
| author: 'Academic Guild', | |
| category: 'writing', | |
| tags: ['academic writing', 'essay', 'research'], | |
| model: 'Qwen/Qwen3-30B-A3B', | |
| systemPrompt: 'You are an experienced academic writing tutor. Help users improve their writing by:\n\n1. **Structure & Organization**: Clear thesis, logical flow, proper transitions\n2. **Clarity & Precision**: Eliminate ambiguity, improve word choice\n3. **Academic Style**: Formal tone, appropriate citations, scholarly voice\n4. **Grammar & Mechanics**: Correct errors, improve sentence variety\n5. **Argument Development**: Strengthen evidence, address counterarguments\n\nProvide specific feedback with examples and rewrite suggestions where helpful.', | |
| temperature: 0.4, | |
| maxTokens: 1200, | |
| likes: 189, | |
| downloads: 856, | |
| isOfficial: false, | |
| createdAt: '2024-01-20' | |
| }, | |
| { | |
| id: 'data-analyst', | |
| name: 'Data Analysis Assistant', | |
| description: 'Helps analyze data, create visualizations, and explain statistical concepts in simple terms.', | |
| author: 'DataPro', | |
| category: 'analysis', | |
| tags: ['data science', 'statistics', 'visualization'], | |
| model: 'Qwen/Qwen3-30B-A3B', | |
| systemPrompt: 'You are a data analysis expert. Help users understand and analyze data by:\n\n1. **Data Exploration**: Identify patterns, outliers, relationships\n2. **Statistical Analysis**: Apply appropriate tests, interpret results\n3. **Visualization**: Suggest effective charts and graphs\n4. **Insights**: Draw meaningful conclusions from data\n5. **Communication**: Explain complex concepts simply\n\nProvide step-by-step analysis and practical recommendations.', | |
| temperature: 0.2, | |
| maxTokens: 1000, | |
| likes: 156, | |
| downloads: 643, | |
| isOfficial: false, | |
| createdAt: '2024-01-25' | |
| }, | |
| { | |
| id: 'creative-writer', | |
| name: 'Creative Writing Coach', | |
| description: 'Inspires creativity and helps develop compelling stories, characters, and narrative techniques.', | |
| author: 'StoryMaster', | |
| category: 'creative', | |
| tags: ['creative writing', 'storytelling', 'fiction'], | |
| model: 'Qwen/Qwen3-30B-A3B', | |
| systemPrompt: 'You are a creative writing coach with expertise in storytelling. Help writers by:\n\n1. **Story Development**: Plot structure, pacing, conflict resolution\n2. **Character Creation**: Compelling personalities, realistic dialogue, character arcs\n3. **World Building**: Consistent settings, atmosphere, details\n4. **Writing Techniques**: Show vs tell, point of view, voice\n5. **Inspiration**: Creative prompts, overcoming writer\'s block\n\nProvide encouraging feedback and concrete suggestions to enhance creativity.', | |
| temperature: 0.8, | |
| maxTokens: 1200, | |
| likes: 312, | |
| downloads: 987, | |
| isOfficial: false, | |
| createdAt: '2024-01-30' | |
| }, | |
| { | |
| id: 'interview-prep', | |
| name: 'Interview Preparation Coach', | |
| description: 'Helps prepare for job interviews with practice questions, answer strategies, and confidence building.', | |
| author: 'CareerBoost', | |
| category: 'career', | |
| tags: ['interview', 'job search', 'career'], | |
| model: 'Qwen/Qwen3-30B-A3B', | |
| systemPrompt: 'You are a professional career coach specializing in interview preparation. Help candidates by:\n\n1. **Question Practice**: Common and behavioral interview questions\n2. **Answer Framework**: STAR method, structured responses\n3. **Company Research**: Industry insights, company-specific preparation\n4. **Confidence Building**: Reducing anxiety, improving presentation\n5. **Follow-up**: Thank you notes, next steps\n\nProvide personalized advice and realistic practice scenarios.', | |
| temperature: 0.5, | |
| maxTokens: 1000, | |
| likes: 278, | |
| downloads: 1456, | |
| isOfficial: true, | |
| createdAt: '2024-02-05' | |
| } | |
| ] | |
| } | |
| export function Models() { | |
| const [models, setModels] = useState<ModelInfo[]>([]) | |
| const [loading, setLoading] = useState(true) | |
| const [modelLoading, setModelLoading] = useState<string | null>(null) | |
| const [savedAssistants, setSavedAssistants] = useState<any[]>([]) | |
| const [communityTemplates] = useState<CommunityTemplate[]>(getCommunityTemplates()) | |
| const [likedTemplates, setLikedTemplates] = useState<string[]>([]) | |
| useEffect(() => { | |
| fetchModels() | |
| loadSavedAssistants() | |
| loadLikedTemplates() | |
| }, []) | |
| const loadSavedAssistants = () => { | |
| try { | |
| const assistants = JSON.parse(localStorage.getItem('savedAssistants') || '[]') | |
| setSavedAssistants(assistants) | |
| } catch (error) { | |
| console.error('Failed to load saved assistants:', error) | |
| setSavedAssistants([]) | |
| } | |
| } | |
| const loadAssistant = (assistant: any) => { | |
| // Store the assistant config in localStorage for the playground to use | |
| localStorage.setItem('loadAssistantConfig', JSON.stringify(assistant)) | |
| // Navigate to playground | |
| window.location.href = '/playground' | |
| } | |
| const deleteAssistant = (assistantId: string) => { | |
| const updatedAssistants = savedAssistants.filter(a => a.id !== assistantId) | |
| setSavedAssistants(updatedAssistants) | |
| localStorage.setItem('savedAssistants', JSON.stringify(updatedAssistants)) | |
| } | |
| const loadLikedTemplates = () => { | |
| try { | |
| const liked = JSON.parse(localStorage.getItem('likedTemplates') || '[]') | |
| setLikedTemplates(liked) | |
| } catch (error) { | |
| console.error('Failed to load liked templates:', error) | |
| } | |
| } | |
| const toggleLikeTemplate = (templateId: string) => { | |
| const updatedLiked = likedTemplates.includes(templateId) | |
| ? likedTemplates.filter(id => id !== templateId) | |
| : [...likedTemplates, templateId] | |
| setLikedTemplates(updatedLiked) | |
| localStorage.setItem('likedTemplates', JSON.stringify(updatedLiked)) | |
| } | |
| const useTemplate = (template: CommunityTemplate) => { | |
| const assistantConfig = { | |
| name: template.name, | |
| description: template.description, | |
| model: template.model, | |
| systemPrompt: template.systemPrompt, | |
| temperature: template.temperature, | |
| maxTokens: template.maxTokens | |
| } | |
| localStorage.setItem('loadAssistantConfig', JSON.stringify(assistantConfig)) | |
| window.location.href = '/playground' | |
| } | |
| const shareMyAssistant = (assistant: any) => { | |
| // In a real implementation, this would submit to a backend | |
| // For now, we'll just show a success message | |
| alert(`"${assistant.name}" has been shared to the community! (This is a demo - in production, it would be submitted for review.)`) | |
| } | |
| const fetchModels = async () => { | |
| try { | |
| const baseUrl = `${window.location.protocol}//${window.location.host}` | |
| const res = await fetch(`${baseUrl}/models`) | |
| if (res.ok) { | |
| const data: ModelsResponse = await res.json() | |
| setModels(data.models) | |
| } | |
| } catch (err) { | |
| console.error('Failed to fetch models:', err) | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| const handleLoadModel = async (modelName: string) => { | |
| setModelLoading(modelName) | |
| try { | |
| const baseUrl = `${window.location.protocol}//${window.location.host}` | |
| const res = await fetch(`${baseUrl}/load-model`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ model_name: modelName }), | |
| }) | |
| if (res.ok) { | |
| await fetchModels() | |
| } | |
| } catch (err) { | |
| console.error('Failed to load model:', err) | |
| } finally { | |
| setModelLoading(null) | |
| } | |
| } | |
| const handleUnloadModel = async (modelName: string) => { | |
| try { | |
| const baseUrl = `${window.location.protocol}//${window.location.host}` | |
| const res = await fetch(`${baseUrl}/unload-model`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ model_name: modelName }), | |
| }) | |
| if (res.ok) { | |
| await fetchModels() | |
| } | |
| } catch (err) { | |
| console.error('Failed to unload model:', err) | |
| } | |
| } | |
| if (loading) { | |
| return ( | |
| <div className="min-h-screen bg-background"> | |
| <div className="border-b"> | |
| <div className="flex h-14 items-center px-6"> | |
| <div className="flex items-center gap-2"> | |
| <BookOpen className="h-5 w-5" /> | |
| <h1 className="text-lg font-semibold">Model Catalog</h1> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-center h-64"> | |
| <Loader2 className="h-8 w-8 animate-spin" /> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div className="min-h-screen bg-background"> | |
| {/* Header */} | |
| <div className="border-b"> | |
| <div className="flex h-14 items-center px-6"> | |
| <div className="flex items-center gap-2"> | |
| <BookOpen className="h-5 w-5" /> | |
| <h1 className="text-lg font-semibold">Model Catalog</h1> | |
| </div> | |
| <div className="ml-auto"> | |
| <Button variant="outline" size="sm" onClick={fetchModels}> | |
| Refresh | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex-1 p-6"> | |
| <div className="max-w-6xl mx-auto"> | |
| {/* Info Card */} | |
| <Card className="bg-blue-50 border-blue-200 mb-6"> | |
| <CardContent className="pt-6"> | |
| <div className="flex items-start gap-3"> | |
| <Info className="h-5 w-5 text-blue-600 mt-0.5" /> | |
| <div> | |
| <h3 className="font-medium text-blue-900">Assistant Library</h3> | |
| <p className="text-sm text-blue-700 mt-1"> | |
| Manage your custom assistants and discover community templates. Create specialized AI helpers for different tasks and workflows. | |
| </p> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| {/* Tabs */} | |
| <Tabs defaultValue="my-assistants" className="space-y-6"> | |
| <TabsList className="grid w-full grid-cols-4"> | |
| <TabsTrigger value="my-assistants" className="flex items-center gap-2"> | |
| <Bot className="h-4 w-4" /> | |
| My Assistants | |
| </TabsTrigger> | |
| <TabsTrigger value="community" className="flex items-center gap-2"> | |
| <Users className="h-4 w-4" /> | |
| Community | |
| </TabsTrigger> | |
| <TabsTrigger value="featured" className="flex items-center gap-2"> | |
| <Star className="h-4 w-4" /> | |
| Featured | |
| </TabsTrigger> | |
| <TabsTrigger value="models" className="flex items-center gap-2"> | |
| <Brain className="h-4 w-4" /> | |
| Models | |
| </TabsTrigger> | |
| </TabsList> | |
| {/* My Assistants Tab */} | |
| <TabsContent value="my-assistants" className="space-y-6"> | |
| {savedAssistants.length > 0 ? ( | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| {savedAssistants.map((assistant) => ( | |
| <AssistantCard | |
| key={assistant.id} | |
| assistant={assistant} | |
| onUse={() => loadAssistant(assistant)} | |
| onDelete={() => deleteAssistant(assistant.id)} | |
| onShare={() => shareMyAssistant(assistant)} | |
| type="personal" | |
| /> | |
| ))} | |
| </div> | |
| ) : ( | |
| <Card className="text-center p-8"> | |
| <Bot className="h-12 w-12 mx-auto text-muted-foreground mb-4" /> | |
| <h3 className="text-lg font-medium mb-2">No assistants yet</h3> | |
| <p className="text-muted-foreground mb-4"> | |
| Create your first assistant by configuring parameters in the Playground and saving it. | |
| </p> | |
| <Button onClick={() => window.location.href = '/playground'}> | |
| Go to Playground | |
| </Button> | |
| </Card> | |
| )} | |
| </TabsContent> | |
| {/* Community Templates Tab */} | |
| <TabsContent value="community" className="space-y-6"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| {communityTemplates.map((template) => ( | |
| <CommunityTemplateCard | |
| key={template.id} | |
| template={template} | |
| isLiked={likedTemplates.includes(template.id)} | |
| onLike={() => toggleLikeTemplate(template.id)} | |
| onUse={() => useTemplate(template)} | |
| /> | |
| ))} | |
| </div> | |
| </TabsContent> | |
| {/* Featured Tab */} | |
| <TabsContent value="featured" className="space-y-6"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| {communityTemplates | |
| .filter(t => t.isOfficial || t.likes > 200) | |
| .map((template) => ( | |
| <CommunityTemplateCard | |
| key={template.id} | |
| template={template} | |
| isLiked={likedTemplates.includes(template.id)} | |
| onLike={() => toggleLikeTemplate(template.id)} | |
| onUse={() => useTemplate(template)} | |
| featured={true} | |
| /> | |
| )) | |
| } | |
| </div> | |
| </TabsContent> | |
| {/* Models Tab */} | |
| <TabsContent value="models" className="space-y-6"> | |
| {/* API Models Section */} | |
| <div> | |
| <h2 className="text-xl font-semibold mb-4 flex items-center gap-2"> | |
| <Cloud className="h-5 w-5" /> | |
| API Models | |
| <Badge variant="outline" className="text-xs">Cloud-Powered</Badge> | |
| </h2> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8"> | |
| {models.filter(m => m.type === 'api').map((model) => ( | |
| <ModelCard | |
| key={model.model_name} | |
| model={model} | |
| modelLoading={modelLoading} | |
| onLoad={handleLoadModel} | |
| onUnload={handleUnloadModel} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Local Models Section */} | |
| <div> | |
| <h2 className="text-xl font-semibold mb-4 flex items-center gap-2"> | |
| <HardDrive className="h-5 w-5" /> | |
| Local Models | |
| <Badge variant="outline" className="text-xs">Privacy-First</Badge> | |
| </h2> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| {models.filter(m => m.type === 'local').map((model) => ( | |
| <ModelCard | |
| key={model.model_name} | |
| model={model} | |
| modelLoading={modelLoading} | |
| onLoad={handleLoadModel} | |
| onUnload={handleUnloadModel} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Stats Card */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Model Statistics</CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-blue-600">{models.length}</div> | |
| <div className="text-sm text-muted-foreground">Available Models</div> | |
| </div> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-green-600"> | |
| {models.filter(m => m.is_loaded).length} | |
| </div> | |
| <div className="text-sm text-muted-foreground">Loaded Models</div> | |
| </div> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-purple-600"> | |
| {models.filter(m => m.supports_thinking).length} | |
| </div> | |
| <div className="text-sm text-muted-foreground">Thinking Models</div> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // Assistant Card Component for personal assistants | |
| function AssistantCard({ | |
| assistant, | |
| onUse, | |
| onDelete, | |
| onShare, | |
| type = "personal" | |
| }: { | |
| assistant: any | |
| onUse: () => void | |
| onDelete: () => void | |
| onShare: () => void | |
| type?: "personal" | "community" | |
| }) { | |
| return ( | |
| <Card className="hover:shadow-md transition-shadow"> | |
| <CardHeader className="pb-3"> | |
| <div className="flex items-start justify-between"> | |
| <div className="flex-1"> | |
| <CardTitle className="text-base">{assistant.name}</CardTitle> | |
| <p className="text-sm text-muted-foreground mt-1"> | |
| {assistant.description || 'No description'} | |
| </p> | |
| </div> | |
| <div className="flex gap-1"> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={onShare} | |
| className="text-blue-600 hover:text-blue-700" | |
| title="Share to community" | |
| > | |
| <Share className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={onDelete} | |
| className="text-destructive hover:text-destructive" | |
| title="Delete assistant" | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </div> | |
| </CardHeader> | |
| <CardContent className="space-y-3"> | |
| <div className="text-xs space-y-1 text-muted-foreground"> | |
| <div><span className="font-medium">Model:</span> {assistant.model}</div> | |
| <div><span className="font-medium">Temperature:</span> {assistant.temperature}</div> | |
| <div><span className="font-medium">Max Tokens:</span> {assistant.maxTokens}</div> | |
| <div><span className="font-medium">Created:</span> {new Date(assistant.createdAt).toLocaleDateString()}</div> | |
| </div> | |
| <Button size="sm" onClick={onUse} className="w-full"> | |
| <MessageSquare className="h-4 w-4 mr-2" /> | |
| Use Assistant | |
| </Button> | |
| </CardContent> | |
| </Card> | |
| ) | |
| } | |
| // Community Template Card Component | |
| function CommunityTemplateCard({ | |
| template, | |
| isLiked, | |
| onLike, | |
| onUse, | |
| featured = false | |
| }: { | |
| template: CommunityTemplate | |
| isLiked: boolean | |
| onLike: () => void | |
| onUse: () => void | |
| featured?: boolean | |
| }) { | |
| return ( | |
| <Card className={`hover:shadow-md transition-shadow ${featured ? 'ring-2 ring-yellow-200' : ''}`}> | |
| <CardHeader className="pb-3"> | |
| <div className="flex items-start justify-between"> | |
| <div className="flex-1"> | |
| <div className="flex items-center gap-2 mb-1"> | |
| <CardTitle className="text-base">{template.name}</CardTitle> | |
| {template.isOfficial && ( | |
| <Badge variant="default" className="text-xs px-2 py-0"> | |
| Official | |
| </Badge> | |
| )} | |
| {featured && ( | |
| <Star className="h-4 w-4 text-yellow-500 fill-current" /> | |
| )} | |
| </div> | |
| <p className="text-xs text-muted-foreground">by {template.author}</p> | |
| <p className="text-sm text-muted-foreground mt-1 line-clamp-2"> | |
| {template.description} | |
| </p> | |
| </div> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={onLike} | |
| className={isLiked ? "text-red-500" : "text-muted-foreground hover:text-red-500"} | |
| title={isLiked ? "Unlike" : "Like"} | |
| > | |
| <Heart className={`h-4 w-4 ${isLiked ? 'fill-current' : ''}`} /> | |
| </Button> | |
| </div> | |
| </CardHeader> | |
| <CardContent className="space-y-3"> | |
| <div className="flex flex-wrap gap-1"> | |
| {template.tags.slice(0, 3).map((tag) => ( | |
| <Badge key={tag} variant="secondary" className="text-xs"> | |
| {tag} | |
| </Badge> | |
| ))} | |
| </div> | |
| <div className="text-xs space-y-1 text-muted-foreground"> | |
| <div><span className="font-medium">Model:</span> {template.model}</div> | |
| <div><span className="font-medium">Temperature:</span> {template.temperature}</div> | |
| <div><span className="font-medium">Max Tokens:</span> {template.maxTokens}</div> | |
| </div> | |
| <div className="flex items-center justify-between text-xs text-muted-foreground"> | |
| <div className="flex items-center gap-3"> | |
| <div className="flex items-center gap-1"> | |
| <Heart className="h-3 w-3" /> | |
| <span>{template.likes}</span> | |
| </div> | |
| <div className="flex items-center gap-1"> | |
| <Download className="h-3 w-3" /> | |
| <span>{template.downloads}</span> | |
| </div> | |
| </div> | |
| <span>{template.createdAt}</span> | |
| </div> | |
| <div className="flex gap-2"> | |
| <Button size="sm" onClick={onUse} className="flex-1"> | |
| <Copy className="h-4 w-4 mr-1" /> | |
| Use Template | |
| </Button> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ) | |
| } | |
| // ModelCard component for reusability | |
| interface ModelCardProps { | |
| model: ModelInfo | |
| modelLoading: string | null | |
| onLoad: (modelName: string) => void | |
| onUnload: (modelName: string) => void | |
| } | |
| function ModelCard({ model, modelLoading, onLoad, onUnload }: ModelCardProps) { | |
| const isApiModel = model.type === 'api' | |
| return ( | |
| <Card className="relative"> | |
| <CardHeader> | |
| <div className="flex items-start justify-between"> | |
| <div className="flex items-center gap-3"> | |
| {isApiModel ? ( | |
| <Cloud className="h-6 w-6 text-blue-500" /> | |
| ) : model.supports_thinking ? ( | |
| <Brain className="h-6 w-6 text-blue-500" /> | |
| ) : ( | |
| <Zap className="h-6 w-6 text-green-500" /> | |
| )} | |
| <div> | |
| <CardTitle className="text-lg">{model.name}</CardTitle> | |
| <div className="flex items-center gap-2 mt-1 flex-wrap"> | |
| {isApiModel ? ( | |
| <Badge variant="default" className="bg-blue-600"> | |
| <Cloud className="h-3 w-3 mr-1" /> | |
| API Model | |
| </Badge> | |
| ) : ( | |
| <Badge variant={model.supports_thinking ? "default" : "secondary"}> | |
| <HardDrive className="h-3 w-3 mr-1" /> | |
| {model.supports_thinking ? "Thinking Model" : "Instruction Model"} | |
| </Badge> | |
| )} | |
| {model.is_loaded && ( | |
| <Badge variant="outline" className="text-green-600 border-green-600"> | |
| <CheckCircle className="h-3 w-3 mr-1" /> | |
| {isApiModel ? "Ready" : "Loaded"} | |
| </Badge> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div> | |
| <p className="text-sm text-muted-foreground mb-2">{model.description}</p> | |
| <div className="flex items-center gap-4 text-xs text-muted-foreground"> | |
| <span>Size: {model.size_gb}</span> | |
| {!isApiModel && <span>Format: Safetensors</span>} | |
| {isApiModel && <span>Type: Cloud API</span>} | |
| </div> | |
| </div> | |
| <div className="space-y-2"> | |
| <h4 className="text-sm font-medium">Capabilities</h4> | |
| <div className="flex flex-wrap gap-2"> | |
| <Badge variant="outline" className="text-xs">Text Generation</Badge> | |
| <Badge variant="outline" className="text-xs">Conversation</Badge> | |
| <Badge variant="outline" className="text-xs">Code</Badge> | |
| {model.supports_thinking && ( | |
| <Badge variant="outline" className="text-xs">Reasoning</Badge> | |
| )} | |
| {isApiModel && model.model_name.includes('vl') && ( | |
| <Badge variant="outline" className="text-xs">Vision</Badge> | |
| )} | |
| </div> | |
| </div> | |
| <div className="pt-2 border-t"> | |
| {model.is_loaded ? ( | |
| <div className="flex gap-2"> | |
| {!isApiModel && ( | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => onUnload(model.model_name)} | |
| className="flex-1" | |
| > | |
| <Trash2 className="h-4 w-4 mr-2" /> | |
| Unload | |
| </Button> | |
| )} | |
| <Button size="sm" className="flex-1" asChild> | |
| <a href="/playground">Use in Playground</a> | |
| </Button> | |
| </div> | |
| ) : ( | |
| <Button | |
| onClick={() => onLoad(model.model_name)} | |
| disabled={modelLoading === model.model_name} | |
| className="w-full" | |
| size="sm" | |
| > | |
| {modelLoading === model.model_name ? ( | |
| <> | |
| <Loader2 className="h-4 w-4 mr-2 animate-spin" /> | |
| {isApiModel ? "Connecting..." : "Loading..."} | |
| </> | |
| ) : ( | |
| <> | |
| {isApiModel ? ( | |
| <Cloud className="h-4 w-4 mr-2" /> | |
| ) : ( | |
| <Download className="h-4 w-4 mr-2" /> | |
| )} | |
| {isApiModel ? "Connect" : "Load Model"} | |
| </> | |
| )} | |
| </Button> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ) | |
| } | |