wu981526092's picture
Add community templates feature with tabs in Models page - includes predefined templates and like functionality
45e0f20
raw
history blame
28.4 kB
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>
)
}