wu981526092 commited on
Commit
1c47f1e
·
1 Parent(s): 7cbaf18

Restructure features: separate Assistants and Community from Model Catalog

Browse files

- Add new sidebar navigation items for My Assistants and Community
- Create dedicated Assistants page for personal assistant management
- Create dedicated Community page for template discovery and sharing
- Simplify Model Catalog to focus only on model management
- Update routing to support new independent pages
- Improve feature separation and user experience

frontend/src/App.tsx CHANGED
@@ -3,6 +3,8 @@ import { Layout } from './components/Layout'
3
  import { Home } from './pages/Home'
4
  import { Playground } from './pages/Playground'
5
  import { Models } from './pages/Models'
 
 
6
  import { Settings } from './pages/Settings'
7
 
8
  function App() {
@@ -13,6 +15,8 @@ function App() {
13
  <Route index element={<Home />} />
14
  <Route path="playground" element={<Playground />} />
15
  <Route path="models" element={<Models />} />
 
 
16
  <Route path="settings" element={<Settings />} />
17
  </Route>
18
  </Routes>
 
3
  import { Home } from './pages/Home'
4
  import { Playground } from './pages/Playground'
5
  import { Models } from './pages/Models'
6
+ import { Assistants } from './pages/Assistants'
7
+ import { Community } from './pages/Community'
8
  import { Settings } from './pages/Settings'
9
 
10
  function App() {
 
15
  <Route index element={<Home />} />
16
  <Route path="playground" element={<Playground />} />
17
  <Route path="models" element={<Models />} />
18
+ <Route path="assistants" element={<Assistants />} />
19
+ <Route path="community" element={<Community />} />
20
  <Route path="settings" element={<Settings />} />
21
  </Route>
22
  </Routes>
frontend/src/components/Sidebar.tsx CHANGED
@@ -5,7 +5,9 @@ import {
5
  BookOpen,
6
  MessageSquare,
7
  Settings,
8
- Brain
 
 
9
  } from 'lucide-react'
10
 
11
  const navigation = [
@@ -25,7 +27,19 @@ const navigation = [
25
  name: 'Model Catalog',
26
  href: '/models',
27
  icon: BookOpen,
28
- description: 'Browse and manage models'
 
 
 
 
 
 
 
 
 
 
 
 
29
  },
30
  {
31
  name: 'Settings',
 
5
  BookOpen,
6
  MessageSquare,
7
  Settings,
8
+ Brain,
9
+ Bot,
10
+ Users
11
  } from 'lucide-react'
12
 
13
  const navigation = [
 
27
  name: 'Model Catalog',
28
  href: '/models',
29
  icon: BookOpen,
30
+ description: 'Browse and manage AI models'
31
+ },
32
+ {
33
+ name: 'My Assistants',
34
+ href: '/assistants',
35
+ icon: Bot,
36
+ description: 'Manage your custom AI assistants'
37
+ },
38
+ {
39
+ name: 'Community',
40
+ href: '/community',
41
+ icon: Users,
42
+ description: 'Discover and share assistant templates'
43
  },
44
  {
45
  name: 'Settings',
frontend/src/pages/Assistants.tsx ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
3
+ import { Button } from '@/components/ui/button'
4
+ import { Badge } from '@/components/ui/badge'
5
+ import {
6
+ Bot,
7
+ MessageSquare,
8
+ Trash2,
9
+ Share,
10
+ Plus,
11
+ Sparkles
12
+ } from 'lucide-react'
13
+
14
+ export function Assistants() {
15
+ const [savedAssistants, setSavedAssistants] = useState<any[]>([])
16
+
17
+ useEffect(() => {
18
+ loadSavedAssistants()
19
+ }, [])
20
+
21
+ const loadSavedAssistants = () => {
22
+ try {
23
+ const assistants = JSON.parse(localStorage.getItem('savedAssistants') || '[]')
24
+ setSavedAssistants(assistants)
25
+ } catch (error) {
26
+ console.error('Failed to load assistants:', error)
27
+ }
28
+ }
29
+
30
+ const loadAssistant = (assistant: any) => {
31
+ localStorage.setItem('loadAssistantConfig', JSON.stringify(assistant))
32
+ window.location.href = '/playground'
33
+ }
34
+
35
+ const deleteAssistant = (assistantId: string) => {
36
+ const updatedAssistants = savedAssistants.filter(a => a.id !== assistantId)
37
+ setSavedAssistants(updatedAssistants)
38
+ localStorage.setItem('savedAssistants', JSON.stringify(updatedAssistants))
39
+ }
40
+
41
+ const shareMyAssistant = (assistant: any) => {
42
+ alert(`"${assistant.name}" has been shared to the community! (This is a demo - in production, it would be submitted for review.)`)
43
+ }
44
+
45
+ const createNewAssistant = () => {
46
+ window.location.href = '/playground'
47
+ }
48
+
49
+ return (
50
+ <div className="min-h-screen bg-background">
51
+ {/* Header */}
52
+ <div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
53
+ <div className="max-w-6xl mx-auto p-6">
54
+ <div className="flex items-center justify-between">
55
+ <div className="flex items-center gap-3">
56
+ <div className="w-8 h-8 bg-purple-600 rounded-lg flex items-center justify-center">
57
+ <Bot className="h-5 w-5 text-white" />
58
+ </div>
59
+ <div>
60
+ <h1 className="text-2xl font-bold">My Assistants</h1>
61
+ <p className="text-sm text-muted-foreground">
62
+ Manage your custom AI assistants and configurations
63
+ </p>
64
+ </div>
65
+ </div>
66
+ <Button onClick={createNewAssistant} className="flex items-center gap-2">
67
+ <Plus className="h-4 w-4" />
68
+ Create New Assistant
69
+ </Button>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <div className="flex-1 p-6">
75
+ <div className="max-w-6xl mx-auto space-y-6">
76
+
77
+ {/* Info Card */}
78
+ <Card className="bg-purple-50 border-purple-200">
79
+ <CardContent className="pt-6">
80
+ <div className="flex items-start gap-3">
81
+ <Sparkles className="h-5 w-5 text-purple-600 mt-0.5" />
82
+ <div>
83
+ <h3 className="font-medium text-purple-900">Custom AI Assistants</h3>
84
+ <p className="text-sm text-purple-700 mt-1">
85
+ Create specialized AI assistants by configuring models, prompts, and parameters in the Playground.
86
+ Save different configurations for various tasks and workflows.
87
+ </p>
88
+ </div>
89
+ </div>
90
+ </CardContent>
91
+ </Card>
92
+
93
+ {/* Assistants Grid */}
94
+ {savedAssistants.length > 0 ? (
95
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
96
+ {savedAssistants.map((assistant) => (
97
+ <AssistantCard
98
+ key={assistant.id}
99
+ assistant={assistant}
100
+ onUse={() => loadAssistant(assistant)}
101
+ onDelete={() => deleteAssistant(assistant.id)}
102
+ onShare={() => shareMyAssistant(assistant)}
103
+ />
104
+ ))}
105
+ </div>
106
+ ) : (
107
+ <Card className="text-center py-16">
108
+ <Bot className="h-16 w-16 mx-auto text-muted-foreground mb-6" />
109
+ <h3 className="text-xl font-medium mb-3">No assistants yet</h3>
110
+ <p className="text-muted-foreground mb-6 max-w-md mx-auto">
111
+ Create your first AI assistant by configuring parameters and prompts in the Playground,
112
+ then saving your configuration for future use.
113
+ </p>
114
+ <Button onClick={createNewAssistant} size="lg" className="flex items-center gap-2 mx-auto">
115
+ <Plus className="h-5 w-5" />
116
+ Create Your First Assistant
117
+ </Button>
118
+ </Card>
119
+ )}
120
+
121
+ {/* Stats Cards */}
122
+ {savedAssistants.length > 0 && (
123
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
124
+ <Card>
125
+ <CardContent className="pt-6">
126
+ <div className="flex items-center justify-between">
127
+ <div>
128
+ <div className="text-2xl font-bold text-purple-600">
129
+ {savedAssistants.length}
130
+ </div>
131
+ <div className="text-sm text-muted-foreground">Total Assistants</div>
132
+ </div>
133
+ <Bot className="h-8 w-8 text-purple-600" />
134
+ </div>
135
+ </CardContent>
136
+ </Card>
137
+
138
+ <Card>
139
+ <CardContent className="pt-6">
140
+ <div className="flex items-center justify-between">
141
+ <div>
142
+ <div className="text-2xl font-bold text-blue-600">
143
+ {new Set(savedAssistants.map(a => a.model)).size}
144
+ </div>
145
+ <div className="text-sm text-muted-foreground">Models Used</div>
146
+ </div>
147
+ <MessageSquare className="h-8 w-8 text-blue-600" />
148
+ </div>
149
+ </CardContent>
150
+ </Card>
151
+
152
+ <Card>
153
+ <CardContent className="pt-6">
154
+ <div className="flex items-center justify-between">
155
+ <div>
156
+ <div className="text-2xl font-bold text-green-600">
157
+ {savedAssistants.filter(a => a.createdAt &&
158
+ new Date(a.createdAt) > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
159
+ ).length}
160
+ </div>
161
+ <div className="text-sm text-muted-foreground">Created This Week</div>
162
+ </div>
163
+ <Sparkles className="h-8 w-8 text-green-600" />
164
+ </div>
165
+ </CardContent>
166
+ </Card>
167
+ </div>
168
+ )}
169
+ </div>
170
+ </div>
171
+ </div>
172
+ )
173
+ }
174
+
175
+ // Assistant Card Component
176
+ function AssistantCard({
177
+ assistant,
178
+ onUse,
179
+ onDelete,
180
+ onShare
181
+ }: {
182
+ assistant: any
183
+ onUse: () => void
184
+ onDelete: () => void
185
+ onShare: () => void
186
+ }) {
187
+ return (
188
+ <Card className="hover:shadow-lg transition-all duration-200 hover:-translate-y-1">
189
+ <CardHeader className="pb-3">
190
+ <div className="flex items-start justify-between">
191
+ <div className="flex-1">
192
+ <CardTitle className="text-base flex items-center gap-2">
193
+ <Bot className="h-4 w-4 text-purple-600" />
194
+ {assistant.name}
195
+ </CardTitle>
196
+ <p className="text-sm text-muted-foreground mt-1">
197
+ {assistant.description || 'No description provided'}
198
+ </p>
199
+ </div>
200
+ <div className="flex gap-1">
201
+ <Button
202
+ variant="ghost"
203
+ size="sm"
204
+ onClick={onShare}
205
+ className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
206
+ title="Share to community"
207
+ >
208
+ <Share className="h-4 w-4" />
209
+ </Button>
210
+ <Button
211
+ variant="ghost"
212
+ size="sm"
213
+ onClick={onDelete}
214
+ className="text-red-500 hover:text-red-600 hover:bg-red-50"
215
+ title="Delete assistant"
216
+ >
217
+ <Trash2 className="h-4 w-4" />
218
+ </Button>
219
+ </div>
220
+ </div>
221
+ </CardHeader>
222
+ <CardContent className="space-y-4">
223
+ {/* Configuration Details */}
224
+ <div className="grid grid-cols-2 gap-3 text-xs">
225
+ <div className="bg-gray-50 rounded p-2">
226
+ <span className="font-medium text-gray-600">Model:</span>
227
+ <p className="text-gray-800 truncate">{assistant.model}</p>
228
+ </div>
229
+ <div className="bg-gray-50 rounded p-2">
230
+ <span className="font-medium text-gray-600">Temperature:</span>
231
+ <p className="text-gray-800">{assistant.temperature}</p>
232
+ </div>
233
+ <div className="bg-gray-50 rounded p-2">
234
+ <span className="font-medium text-gray-600">Max Tokens:</span>
235
+ <p className="text-gray-800">{assistant.maxTokens}</p>
236
+ </div>
237
+ <div className="bg-gray-50 rounded p-2">
238
+ <span className="font-medium text-gray-600">Created:</span>
239
+ <p className="text-gray-800">{new Date(assistant.createdAt).toLocaleDateString()}</p>
240
+ </div>
241
+ </div>
242
+
243
+ {/* System Prompt Preview */}
244
+ {assistant.systemPrompt && (
245
+ <div className="bg-blue-50 rounded p-2">
246
+ <span className="text-xs font-medium text-blue-700">System Prompt:</span>
247
+ <p className="text-xs text-blue-600 mt-1 line-clamp-2">
248
+ {assistant.systemPrompt}
249
+ </p>
250
+ </div>
251
+ )}
252
+
253
+ {/* Action Button */}
254
+ <Button onClick={onUse} className="w-full flex items-center gap-2">
255
+ <MessageSquare className="h-4 w-4" />
256
+ Use Assistant
257
+ </Button>
258
+ </CardContent>
259
+ </Card>
260
+ )
261
+ }
frontend/src/pages/Community.tsx ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
3
+ import { Button } from '@/components/ui/button'
4
+ import { Badge } from '@/components/ui/badge'
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
6
+ import {
7
+ Users,
8
+ Star,
9
+ Heart,
10
+ Download,
11
+ Copy,
12
+ Sparkles,
13
+ TrendingUp,
14
+ Award
15
+ } from 'lucide-react'
16
+
17
+ // Community template interface
18
+ interface CommunityTemplate {
19
+ id: string
20
+ name: string
21
+ description: string
22
+ author: string
23
+ category: string
24
+ tags: string[]
25
+ model: string
26
+ systemPrompt: string
27
+ temperature: number
28
+ maxTokens: number
29
+ likes: number
30
+ downloads: number
31
+ isOfficial: boolean
32
+ createdAt: string
33
+ }
34
+
35
+ // Get predefined community templates
36
+ function getCommunityTemplates(): CommunityTemplate[] {
37
+ return [
38
+ {
39
+ id: 'code-reviewer',
40
+ name: 'Code Review Expert',
41
+ description: 'Professional code reviewer that provides detailed analysis, suggests improvements, and identifies potential issues.',
42
+ author: 'EdgeLLM Team',
43
+ category: 'coding',
44
+ tags: ['code review', 'programming', 'best practices'],
45
+ model: 'Qwen/Qwen3-30B-A3B',
46
+ 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.',
47
+ temperature: 0.3,
48
+ maxTokens: 1500,
49
+ likes: 245,
50
+ downloads: 1200,
51
+ isOfficial: true,
52
+ createdAt: '2024-01-15'
53
+ },
54
+ {
55
+ id: 'writing-tutor',
56
+ name: 'Academic Writing Tutor',
57
+ description: 'Helps improve academic writing with structure suggestions, grammar corrections, and clarity enhancements.',
58
+ author: 'Academic Guild',
59
+ category: 'writing',
60
+ tags: ['academic writing', 'essay', 'research'],
61
+ model: 'Qwen/Qwen3-30B-A3B',
62
+ 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.',
63
+ temperature: 0.4,
64
+ maxTokens: 1200,
65
+ likes: 189,
66
+ downloads: 856,
67
+ isOfficial: false,
68
+ createdAt: '2024-01-20'
69
+ },
70
+ {
71
+ id: 'data-analyst',
72
+ name: 'Data Analysis Assistant',
73
+ description: 'Helps analyze data, create visualizations, and explain statistical concepts in simple terms.',
74
+ author: 'DataPro',
75
+ category: 'analysis',
76
+ tags: ['data science', 'statistics', 'visualization'],
77
+ model: 'Qwen/Qwen3-30B-A3B',
78
+ 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.',
79
+ temperature: 0.2,
80
+ maxTokens: 1000,
81
+ likes: 156,
82
+ downloads: 643,
83
+ isOfficial: false,
84
+ createdAt: '2024-01-25'
85
+ },
86
+ {
87
+ id: 'creative-writer',
88
+ name: 'Creative Writing Coach',
89
+ description: 'Inspires creativity and helps develop compelling stories, characters, and narrative techniques.',
90
+ author: 'StoryMaster',
91
+ category: 'creative',
92
+ tags: ['creative writing', 'storytelling', 'fiction'],
93
+ model: 'Qwen/Qwen3-30B-A3B',
94
+ 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.',
95
+ temperature: 0.8,
96
+ maxTokens: 1200,
97
+ likes: 312,
98
+ downloads: 987,
99
+ isOfficial: false,
100
+ createdAt: '2024-01-30'
101
+ },
102
+ {
103
+ id: 'interview-prep',
104
+ name: 'Interview Preparation Coach',
105
+ description: 'Helps prepare for job interviews with practice questions, answer strategies, and confidence building.',
106
+ author: 'CareerBoost',
107
+ category: 'career',
108
+ tags: ['interview', 'job search', 'career'],
109
+ model: 'Qwen/Qwen3-30B-A3B',
110
+ 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.',
111
+ temperature: 0.5,
112
+ maxTokens: 1000,
113
+ likes: 278,
114
+ downloads: 1456,
115
+ isOfficial: true,
116
+ createdAt: '2024-02-05'
117
+ }
118
+ ]
119
+ }
120
+
121
+ export function Community() {
122
+ const [communityTemplates] = useState<CommunityTemplate[]>(getCommunityTemplates())
123
+ const [likedTemplates, setLikedTemplates] = useState<string[]>([])
124
+
125
+ useEffect(() => {
126
+ loadLikedTemplates()
127
+ }, [])
128
+
129
+ const loadLikedTemplates = () => {
130
+ try {
131
+ const liked = JSON.parse(localStorage.getItem('likedTemplates') || '[]')
132
+ setLikedTemplates(liked)
133
+ } catch (error) {
134
+ console.error('Failed to load liked templates:', error)
135
+ }
136
+ }
137
+
138
+ const toggleLikeTemplate = (templateId: string) => {
139
+ const updatedLiked = likedTemplates.includes(templateId)
140
+ ? likedTemplates.filter(id => id !== templateId)
141
+ : [...likedTemplates, templateId]
142
+
143
+ setLikedTemplates(updatedLiked)
144
+ localStorage.setItem('likedTemplates', JSON.stringify(updatedLiked))
145
+ }
146
+
147
+ const useTemplate = (template: CommunityTemplate) => {
148
+ const assistantConfig = {
149
+ name: template.name,
150
+ description: template.description,
151
+ model: template.model,
152
+ systemPrompt: template.systemPrompt,
153
+ temperature: template.temperature,
154
+ maxTokens: template.maxTokens
155
+ }
156
+
157
+ localStorage.setItem('loadAssistantConfig', JSON.stringify(assistantConfig))
158
+ window.location.href = '/playground'
159
+ }
160
+
161
+ const featuredTemplates = communityTemplates.filter(t => t.isOfficial || t.likes > 200)
162
+ const totalLikes = communityTemplates.reduce((sum, t) => sum + t.likes, 0)
163
+ const totalDownloads = communityTemplates.reduce((sum, t) => sum + t.downloads, 0)
164
+
165
+ return (
166
+ <div className="min-h-screen bg-background">
167
+ {/* Header */}
168
+ <div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
169
+ <div className="max-w-6xl mx-auto p-6">
170
+ <div className="flex items-center justify-between">
171
+ <div className="flex items-center gap-3">
172
+ <div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
173
+ <Users className="h-5 w-5 text-white" />
174
+ </div>
175
+ <div>
176
+ <h1 className="text-2xl font-bold">Community Templates</h1>
177
+ <p className="text-sm text-muted-foreground">
178
+ Discover and share AI assistant templates created by the community
179
+ </p>
180
+ </div>
181
+ </div>
182
+ <div className="flex items-center gap-4 text-sm text-muted-foreground">
183
+ <div className="flex items-center gap-1">
184
+ <Heart className="h-4 w-4" />
185
+ <span>{totalLikes.toLocaleString()} likes</span>
186
+ </div>
187
+ <div className="flex items-center gap-1">
188
+ <Download className="h-4 w-4" />
189
+ <span>{totalDownloads.toLocaleString()} downloads</span>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ <div className="flex-1 p-6">
197
+ <div className="max-w-6xl mx-auto space-y-6">
198
+
199
+ {/* Info Card */}
200
+ <Card className="bg-blue-50 border-blue-200">
201
+ <CardContent className="pt-6">
202
+ <div className="flex items-start gap-3">
203
+ <Sparkles className="h-5 w-5 text-blue-600 mt-0.5" />
204
+ <div>
205
+ <h3 className="font-medium text-blue-900">Community-Powered AI</h3>
206
+ <p className="text-sm text-blue-700 mt-1">
207
+ Explore specialized AI assistant templates created by experts and the community.
208
+ Click "Use Template" to load any configuration directly into your Playground.
209
+ </p>
210
+ </div>
211
+ </div>
212
+ </CardContent>
213
+ </Card>
214
+
215
+ {/* Tabs */}
216
+ <Tabs defaultValue="all" className="space-y-6">
217
+ <TabsList className="grid w-full grid-cols-3">
218
+ <TabsTrigger value="all" className="flex items-center gap-2">
219
+ <Users className="h-4 w-4" />
220
+ All Templates
221
+ </TabsTrigger>
222
+ <TabsTrigger value="featured" className="flex items-center gap-2">
223
+ <Star className="h-4 w-4" />
224
+ Featured
225
+ </TabsTrigger>
226
+ <TabsTrigger value="liked" className="flex items-center gap-2">
227
+ <Heart className="h-4 w-4" />
228
+ My Likes
229
+ </TabsTrigger>
230
+ </TabsList>
231
+
232
+ {/* All Templates Tab */}
233
+ <TabsContent value="all" className="space-y-6">
234
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
235
+ {communityTemplates.map((template) => (
236
+ <CommunityTemplateCard
237
+ key={template.id}
238
+ template={template}
239
+ isLiked={likedTemplates.includes(template.id)}
240
+ onLike={() => toggleLikeTemplate(template.id)}
241
+ onUse={() => useTemplate(template)}
242
+ />
243
+ ))}
244
+ </div>
245
+ </TabsContent>
246
+
247
+ {/* Featured Templates Tab */}
248
+ <TabsContent value="featured" className="space-y-6">
249
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
250
+ {featuredTemplates.map((template) => (
251
+ <CommunityTemplateCard
252
+ key={template.id}
253
+ template={template}
254
+ isLiked={likedTemplates.includes(template.id)}
255
+ onLike={() => toggleLikeTemplate(template.id)}
256
+ onUse={() => useTemplate(template)}
257
+ featured={true}
258
+ />
259
+ ))}
260
+ </div>
261
+ </TabsContent>
262
+
263
+ {/* Liked Templates Tab */}
264
+ <TabsContent value="liked" className="space-y-6">
265
+ {likedTemplates.length > 0 ? (
266
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
267
+ {communityTemplates
268
+ .filter(t => likedTemplates.includes(t.id))
269
+ .map((template) => (
270
+ <CommunityTemplateCard
271
+ key={template.id}
272
+ template={template}
273
+ isLiked={true}
274
+ onLike={() => toggleLikeTemplate(template.id)}
275
+ onUse={() => useTemplate(template)}
276
+ />
277
+ ))
278
+ }
279
+ </div>
280
+ ) : (
281
+ <Card className="text-center py-16">
282
+ <Heart className="h-16 w-16 mx-auto text-muted-foreground mb-6" />
283
+ <h3 className="text-xl font-medium mb-3">No liked templates yet</h3>
284
+ <p className="text-muted-foreground mb-6 max-w-md mx-auto">
285
+ Explore the community templates and click the heart icon to save your favorites for easy access.
286
+ </p>
287
+ </Card>
288
+ )}
289
+ </TabsContent>
290
+ </Tabs>
291
+
292
+ {/* Stats Cards */}
293
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
294
+ <Card>
295
+ <CardContent className="pt-6">
296
+ <div className="flex items-center justify-between">
297
+ <div>
298
+ <div className="text-2xl font-bold text-blue-600">
299
+ {communityTemplates.length}
300
+ </div>
301
+ <div className="text-sm text-muted-foreground">Templates</div>
302
+ </div>
303
+ <Copy className="h-8 w-8 text-blue-600" />
304
+ </div>
305
+ </CardContent>
306
+ </Card>
307
+
308
+ <Card>
309
+ <CardContent className="pt-6">
310
+ <div className="flex items-center justify-between">
311
+ <div>
312
+ <div className="text-2xl font-bold text-green-600">
313
+ {communityTemplates.filter(t => t.isOfficial).length}
314
+ </div>
315
+ <div className="text-sm text-muted-foreground">Official</div>
316
+ </div>
317
+ <Award className="h-8 w-8 text-green-600" />
318
+ </div>
319
+ </CardContent>
320
+ </Card>
321
+
322
+ <Card>
323
+ <CardContent className="pt-6">
324
+ <div className="flex items-center justify-between">
325
+ <div>
326
+ <div className="text-2xl font-bold text-red-500">
327
+ {totalLikes.toLocaleString()}
328
+ </div>
329
+ <div className="text-sm text-muted-foreground">Total Likes</div>
330
+ </div>
331
+ <Heart className="h-8 w-8 text-red-500" />
332
+ </div>
333
+ </CardContent>
334
+ </Card>
335
+
336
+ <Card>
337
+ <CardContent className="pt-6">
338
+ <div className="flex items-center justify-between">
339
+ <div>
340
+ <div className="text-2xl font-bold text-purple-600">
341
+ {totalDownloads.toLocaleString()}
342
+ </div>
343
+ <div className="text-sm text-muted-foreground">Downloads</div>
344
+ </div>
345
+ <TrendingUp className="h-8 w-8 text-purple-600" />
346
+ </div>
347
+ </CardContent>
348
+ </Card>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ )
354
+ }
355
+
356
+ // Community Template Card Component
357
+ function CommunityTemplateCard({
358
+ template,
359
+ isLiked,
360
+ onLike,
361
+ onUse,
362
+ featured = false
363
+ }: {
364
+ template: CommunityTemplate
365
+ isLiked: boolean
366
+ onLike: () => void
367
+ onUse: () => void
368
+ featured?: boolean
369
+ }) {
370
+ return (
371
+ <Card className={`hover:shadow-lg transition-all duration-200 hover:-translate-y-1 ${featured ? 'ring-2 ring-yellow-200 shadow-md' : ''}`}>
372
+ <CardHeader className="pb-3">
373
+ <div className="flex items-start justify-between">
374
+ <div className="flex-1">
375
+ <div className="flex items-center gap-2 mb-1">
376
+ <CardTitle className="text-base">{template.name}</CardTitle>
377
+ {template.isOfficial && (
378
+ <Badge variant="default" className="text-xs px-2 py-0 bg-green-600">
379
+ <Award className="h-3 w-3 mr-1" />
380
+ Official
381
+ </Badge>
382
+ )}
383
+ {featured && (
384
+ <Star className="h-4 w-4 text-yellow-500 fill-current" />
385
+ )}
386
+ </div>
387
+ <p className="text-xs text-blue-600 font-medium">by {template.author}</p>
388
+ <p className="text-sm text-muted-foreground mt-2 line-clamp-2">
389
+ {template.description}
390
+ </p>
391
+ </div>
392
+ <Button
393
+ variant="ghost"
394
+ size="sm"
395
+ onClick={onLike}
396
+ className={isLiked ? "text-red-500" : "text-muted-foreground hover:text-red-500"}
397
+ title={isLiked ? "Unlike" : "Like"}
398
+ >
399
+ <Heart className={`h-4 w-4 ${isLiked ? 'fill-current' : ''}`} />
400
+ </Button>
401
+ </div>
402
+ </CardHeader>
403
+ <CardContent className="space-y-4">
404
+ {/* Tags */}
405
+ <div className="flex flex-wrap gap-1">
406
+ {template.tags.slice(0, 3).map((tag) => (
407
+ <Badge key={tag} variant="secondary" className="text-xs">
408
+ {tag}
409
+ </Badge>
410
+ ))}
411
+ </div>
412
+
413
+ {/* Configuration */}
414
+ <div className="grid grid-cols-2 gap-2 text-xs">
415
+ <div className="bg-gray-50 rounded p-2">
416
+ <span className="font-medium text-gray-600">Temperature:</span>
417
+ <p className="text-gray-800">{template.temperature}</p>
418
+ </div>
419
+ <div className="bg-gray-50 rounded p-2">
420
+ <span className="font-medium text-gray-600">Max Tokens:</span>
421
+ <p className="text-gray-800">{template.maxTokens}</p>
422
+ </div>
423
+ </div>
424
+
425
+ {/* Stats & Action */}
426
+ <div className="flex items-center justify-between">
427
+ <div className="flex items-center gap-4 text-xs text-muted-foreground">
428
+ <div className="flex items-center gap-1">
429
+ <Heart className="h-3 w-3" />
430
+ <span>{template.likes}</span>
431
+ </div>
432
+ <div className="flex items-center gap-1">
433
+ <Download className="h-3 w-3" />
434
+ <span>{template.downloads}</span>
435
+ </div>
436
+ </div>
437
+ <Button size="sm" onClick={onUse} className="flex items-center gap-1">
438
+ <Copy className="h-3 w-3" />
439
+ Use
440
+ </Button>
441
+ </div>
442
+ </CardContent>
443
+ </Card>
444
+ )
445
+ }
frontend/src/pages/Models.tsx CHANGED
@@ -2,7 +2,6 @@ import { useState, useEffect } from 'react'
2
  import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
3
  import { Button } from '@/components/ui/button'
4
  import { Badge } from '@/components/ui/badge'
5
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
6
  import {
7
  BookOpen,
8
  Brain,
@@ -13,21 +12,14 @@ import {
13
  Info,
14
  CheckCircle,
15
  Cloud,
16
- HardDrive,
17
- Bot,
18
- MessageSquare,
19
- Users,
20
- Star,
21
- Heart,
22
- Share,
23
- Copy
24
  } from 'lucide-react'
25
 
26
  interface ModelInfo {
27
  model_name: string
28
  name: string
29
  supports_thinking: boolean
30
- description: string
31
  size_gb: string
32
  is_loaded: boolean
33
  type: 'local' | 'api'
@@ -38,251 +30,87 @@ interface ModelsResponse {
38
  current_model: string
39
  }
40
 
41
- // Community template interface
42
- interface CommunityTemplate {
43
- id: string
44
- name: string
45
- description: string
46
- author: string
47
- category: string
48
- tags: string[]
49
- model: string
50
- systemPrompt: string
51
- temperature: number
52
- maxTokens: number
53
- likes: number
54
- downloads: number
55
- isOfficial: boolean
56
- createdAt: string
57
- }
58
-
59
- // Get predefined community templates
60
- function getCommunityTemplates(): CommunityTemplate[] {
61
- return [
62
- {
63
- id: 'code-reviewer',
64
- name: 'Code Review Expert',
65
- description: 'Professional code reviewer that provides detailed analysis, suggests improvements, and identifies potential issues.',
66
- author: 'EdgeLLM Team',
67
- category: 'coding',
68
- tags: ['code review', 'programming', 'best practices'],
69
- model: 'Qwen/Qwen3-30B-A3B',
70
- 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.',
71
- temperature: 0.3,
72
- maxTokens: 1500,
73
- likes: 245,
74
- downloads: 1200,
75
- isOfficial: true,
76
- createdAt: '2024-01-15'
77
- },
78
- {
79
- id: 'writing-tutor',
80
- name: 'Academic Writing Tutor',
81
- description: 'Helps improve academic writing with structure suggestions, grammar corrections, and clarity enhancements.',
82
- author: 'Academic Guild',
83
- category: 'writing',
84
- tags: ['academic writing', 'essay', 'research'],
85
- model: 'Qwen/Qwen3-30B-A3B',
86
- 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.',
87
- temperature: 0.4,
88
- maxTokens: 1200,
89
- likes: 189,
90
- downloads: 856,
91
- isOfficial: false,
92
- createdAt: '2024-01-20'
93
- },
94
- {
95
- id: 'data-analyst',
96
- name: 'Data Analysis Assistant',
97
- description: 'Helps analyze data, create visualizations, and explain statistical concepts in simple terms.',
98
- author: 'DataPro',
99
- category: 'analysis',
100
- tags: ['data science', 'statistics', 'visualization'],
101
- model: 'Qwen/Qwen3-30B-A3B',
102
- 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.',
103
- temperature: 0.2,
104
- maxTokens: 1000,
105
- likes: 156,
106
- downloads: 643,
107
- isOfficial: false,
108
- createdAt: '2024-01-25'
109
- },
110
- {
111
- id: 'creative-writer',
112
- name: 'Creative Writing Coach',
113
- description: 'Inspires creativity and helps develop compelling stories, characters, and narrative techniques.',
114
- author: 'StoryMaster',
115
- category: 'creative',
116
- tags: ['creative writing', 'storytelling', 'fiction'],
117
- model: 'Qwen/Qwen3-30B-A3B',
118
- 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.',
119
- temperature: 0.8,
120
- maxTokens: 1200,
121
- likes: 312,
122
- downloads: 987,
123
- isOfficial: false,
124
- createdAt: '2024-01-30'
125
- },
126
- {
127
- id: 'interview-prep',
128
- name: 'Interview Preparation Coach',
129
- description: 'Helps prepare for job interviews with practice questions, answer strategies, and confidence building.',
130
- author: 'CareerBoost',
131
- category: 'career',
132
- tags: ['interview', 'job search', 'career'],
133
- model: 'Qwen/Qwen3-30B-A3B',
134
- 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.',
135
- temperature: 0.5,
136
- maxTokens: 1000,
137
- likes: 278,
138
- downloads: 1456,
139
- isOfficial: true,
140
- createdAt: '2024-02-05'
141
- }
142
- ]
143
- }
144
-
145
  export function Models() {
146
  const [models, setModels] = useState<ModelInfo[]>([])
147
  const [loading, setLoading] = useState(true)
148
  const [modelLoading, setModelLoading] = useState<string | null>(null)
149
- const [savedAssistants, setSavedAssistants] = useState<any[]>([])
150
- const [communityTemplates] = useState<CommunityTemplate[]>(getCommunityTemplates())
151
- const [likedTemplates, setLikedTemplates] = useState<string[]>([])
152
 
153
  useEffect(() => {
154
  fetchModels()
155
- loadSavedAssistants()
156
- loadLikedTemplates()
157
  }, [])
158
 
159
- const loadSavedAssistants = () => {
160
- try {
161
- const assistants = JSON.parse(localStorage.getItem('savedAssistants') || '[]')
162
- setSavedAssistants(assistants)
163
- } catch (error) {
164
- console.error('Failed to load saved assistants:', error)
165
- setSavedAssistants([])
166
- }
167
- }
168
-
169
- const loadAssistant = (assistant: any) => {
170
- // Store the assistant config in localStorage for the playground to use
171
- localStorage.setItem('loadAssistantConfig', JSON.stringify(assistant))
172
- // Navigate to playground
173
- window.location.href = '/playground'
174
- }
175
-
176
- const deleteAssistant = (assistantId: string) => {
177
- const updatedAssistants = savedAssistants.filter(a => a.id !== assistantId)
178
- setSavedAssistants(updatedAssistants)
179
- localStorage.setItem('savedAssistants', JSON.stringify(updatedAssistants))
180
- }
181
-
182
- const loadLikedTemplates = () => {
183
- try {
184
- const liked = JSON.parse(localStorage.getItem('likedTemplates') || '[]')
185
- setLikedTemplates(liked)
186
- } catch (error) {
187
- console.error('Failed to load liked templates:', error)
188
- }
189
- }
190
-
191
- const toggleLikeTemplate = (templateId: string) => {
192
- const updatedLiked = likedTemplates.includes(templateId)
193
- ? likedTemplates.filter(id => id !== templateId)
194
- : [...likedTemplates, templateId]
195
-
196
- setLikedTemplates(updatedLiked)
197
- localStorage.setItem('likedTemplates', JSON.stringify(updatedLiked))
198
- }
199
-
200
- const useTemplate = (template: CommunityTemplate) => {
201
- const assistantConfig = {
202
- name: template.name,
203
- description: template.description,
204
- model: template.model,
205
- systemPrompt: template.systemPrompt,
206
- temperature: template.temperature,
207
- maxTokens: template.maxTokens
208
- }
209
-
210
- localStorage.setItem('loadAssistantConfig', JSON.stringify(assistantConfig))
211
- window.location.href = '/playground'
212
- }
213
-
214
- const shareMyAssistant = (assistant: any) => {
215
- // In a real implementation, this would submit to a backend
216
- // For now, we'll just show a success message
217
- alert(`"${assistant.name}" has been shared to the community! (This is a demo - in production, it would be submitted for review.)`)
218
- }
219
-
220
  const fetchModels = async () => {
221
  try {
222
  const baseUrl = `${window.location.protocol}//${window.location.host}`
223
  const res = await fetch(`${baseUrl}/models`)
224
- if (res.ok) {
225
- const data: ModelsResponse = await res.json()
226
- setModels(data.models)
227
  }
228
- } catch (err) {
229
- console.error('Failed to fetch models:', err)
 
 
 
230
  } finally {
231
  setLoading(false)
232
  }
233
  }
234
 
235
- const handleLoadModel = async (modelName: string) => {
236
  setModelLoading(modelName)
237
  try {
238
  const baseUrl = `${window.location.protocol}//${window.location.host}`
239
  const res = await fetch(`${baseUrl}/load-model`, {
240
  method: 'POST',
241
- headers: { 'Content-Type': 'application/json' },
 
 
242
  body: JSON.stringify({ model_name: modelName }),
243
  })
244
-
245
- if (res.ok) {
246
- await fetchModels()
247
  }
248
- } catch (err) {
249
- console.error('Failed to load model:', err)
 
 
 
250
  } finally {
251
  setModelLoading(null)
252
  }
253
  }
254
 
255
- const handleUnloadModel = async (modelName: string) => {
 
256
  try {
257
  const baseUrl = `${window.location.protocol}//${window.location.host}`
258
  const res = await fetch(`${baseUrl}/unload-model`, {
259
  method: 'POST',
260
- headers: { 'Content-Type': 'application/json' },
 
 
261
  body: JSON.stringify({ model_name: modelName }),
262
  })
263
-
264
- if (res.ok) {
265
- await fetchModels()
266
  }
267
- } catch (err) {
268
- console.error('Failed to unload model:', err)
 
 
 
 
 
269
  }
270
  }
271
 
272
  if (loading) {
273
  return (
274
- <div className="min-h-screen bg-background">
275
- <div className="border-b">
276
- <div className="flex h-14 items-center px-6">
277
- <div className="flex items-center gap-2">
278
- <BookOpen className="h-5 w-5" />
279
- <h1 className="text-lg font-semibold">Model Catalog</h1>
280
- </div>
281
- </div>
282
- </div>
283
- <div className="flex items-center justify-center h-64">
284
- <Loader2 className="h-8 w-8 animate-spin" />
285
- </div>
286
  </div>
287
  )
288
  }
@@ -290,138 +118,65 @@ export function Models() {
290
  return (
291
  <div className="min-h-screen bg-background">
292
  {/* Header */}
293
- <div className="border-b">
294
- <div className="flex h-14 items-center px-6">
295
- <div className="flex items-center gap-2">
296
- <BookOpen className="h-5 w-5" />
297
- <h1 className="text-lg font-semibold">Model Catalog</h1>
298
- </div>
299
- <div className="ml-auto">
300
- <Button variant="outline" size="sm" onClick={fetchModels}>
301
- Refresh
 
 
 
 
 
 
 
 
 
 
 
302
  </Button>
303
  </div>
304
  </div>
305
  </div>
306
 
307
  <div className="flex-1 p-6">
308
- <div className="max-w-6xl mx-auto">
309
 
310
  {/* Info Card */}
311
- <Card className="bg-blue-50 border-blue-200 mb-6">
312
  <CardContent className="pt-6">
313
  <div className="flex items-start gap-3">
314
  <Info className="h-5 w-5 text-blue-600 mt-0.5" />
315
  <div>
316
- <h3 className="font-medium text-blue-900">Assistant Library</h3>
317
  <p className="text-sm text-blue-700 mt-1">
318
- Manage your custom assistants and discover community templates. Create specialized AI helpers for different tasks and workflows.
 
319
  </p>
320
  </div>
321
  </div>
322
  </CardContent>
323
  </Card>
324
 
325
- {/* Tabs */}
326
- <Tabs defaultValue="my-assistants" className="space-y-6">
327
- <TabsList className="grid w-full grid-cols-4">
328
- <TabsTrigger value="my-assistants" className="flex items-center gap-2">
329
- <Bot className="h-4 w-4" />
330
- My Assistants
331
- </TabsTrigger>
332
- <TabsTrigger value="community" className="flex items-center gap-2">
333
- <Users className="h-4 w-4" />
334
- Community
335
- </TabsTrigger>
336
- <TabsTrigger value="featured" className="flex items-center gap-2">
337
- <Star className="h-4 w-4" />
338
- Featured
339
- </TabsTrigger>
340
- <TabsTrigger value="models" className="flex items-center gap-2">
341
- <Brain className="h-4 w-4" />
342
- Models
343
- </TabsTrigger>
344
- </TabsList>
345
-
346
- {/* My Assistants Tab */}
347
- <TabsContent value="my-assistants" className="space-y-6">
348
- {savedAssistants.length > 0 ? (
349
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
350
- {savedAssistants.map((assistant) => (
351
- <AssistantCard
352
- key={assistant.id}
353
- assistant={assistant}
354
- onUse={() => loadAssistant(assistant)}
355
- onDelete={() => deleteAssistant(assistant.id)}
356
- onShare={() => shareMyAssistant(assistant)}
357
- />
358
- ))}
359
- </div>
360
- ) : (
361
- <Card className="text-center p-8">
362
- <Bot className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
363
- <h3 className="text-lg font-medium mb-2">No assistants yet</h3>
364
- <p className="text-muted-foreground mb-4">
365
- Create your first assistant by configuring parameters in the Playground and saving it.
366
- </p>
367
- <Button onClick={() => window.location.href = '/playground'}>
368
- Go to Playground
369
- </Button>
370
- </Card>
371
- )}
372
- </TabsContent>
373
-
374
- {/* Community Templates Tab */}
375
- <TabsContent value="community" className="space-y-6">
376
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
377
- {communityTemplates.map((template) => (
378
- <CommunityTemplateCard
379
- key={template.id}
380
- template={template}
381
- isLiked={likedTemplates.includes(template.id)}
382
- onLike={() => toggleLikeTemplate(template.id)}
383
- onUse={() => useTemplate(template)}
384
- />
385
- ))}
386
- </div>
387
- </TabsContent>
388
-
389
- {/* Featured Tab */}
390
- <TabsContent value="featured" className="space-y-6">
391
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
392
- {communityTemplates
393
- .filter(t => t.isOfficial || t.likes > 200)
394
- .map((template) => (
395
- <CommunityTemplateCard
396
- key={template.id}
397
- template={template}
398
- isLiked={likedTemplates.includes(template.id)}
399
- onLike={() => toggleLikeTemplate(template.id)}
400
- onUse={() => useTemplate(template)}
401
- featured={true}
402
- />
403
- ))
404
- }
405
- </div>
406
- </TabsContent>
407
-
408
- {/* Models Tab */}
409
- <TabsContent value="models" className="space-y-6">
410
- {/* API Models Section */}
411
- <div>
412
- <h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
413
- <Cloud className="h-5 w-5" />
414
  API Models
415
  <Badge variant="outline" className="text-xs">Cloud-Powered</Badge>
416
  </h2>
417
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
418
  {models.filter(m => m.type === 'api').map((model) => (
419
  <ModelCard
420
- key={model.model_name}
421
- model={model}
422
  modelLoading={modelLoading}
423
- onLoad={handleLoadModel}
424
- onUnload={handleUnloadModel}
425
  />
426
  ))}
427
  </div>
@@ -432,34 +187,45 @@ export function Models() {
432
  <h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
433
  <HardDrive className="h-5 w-5" />
434
  Local Models
435
- <Badge variant="outline" className="text-xs">Privacy-First</Badge>
436
  </h2>
437
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
438
  {models.filter(m => m.type === 'local').map((model) => (
439
  <ModelCard
440
- key={model.model_name}
441
- model={model}
442
  modelLoading={modelLoading}
443
- onLoad={handleLoadModel}
444
- onUnload={handleUnloadModel}
445
  />
446
  ))}
447
  </div>
448
  </div>
449
 
450
- {/* Stats Card */}
451
  <Card>
452
  <CardHeader>
453
- <CardTitle>Model Statistics</CardTitle>
 
 
 
454
  </CardHeader>
455
  <CardContent>
456
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
457
  <div className="text-center">
458
- <div className="text-2xl font-bold text-blue-600">{models.length}</div>
459
- <div className="text-sm text-muted-foreground">Available Models</div>
 
 
460
  </div>
461
  <div className="text-center">
462
  <div className="text-2xl font-bold text-green-600">
 
 
 
 
 
 
463
  {models.filter(m => m.is_loaded).length}
464
  </div>
465
  <div className="text-sm text-muted-foreground">Loaded Models</div>
@@ -473,160 +239,12 @@ export function Models() {
473
  </div>
474
  </CardContent>
475
  </Card>
476
- </TabsContent>
477
- </Tabs>
478
  </div>
479
  </div>
480
  </div>
481
  )
482
  }
483
 
484
- // Assistant Card Component for personal assistants
485
- function AssistantCard({
486
- assistant,
487
- onUse,
488
- onDelete,
489
- onShare
490
- }: {
491
- assistant: any
492
- onUse: () => void
493
- onDelete: () => void
494
- onShare: () => void
495
- }) {
496
- return (
497
- <Card className="hover:shadow-md transition-shadow">
498
- <CardHeader className="pb-3">
499
- <div className="flex items-start justify-between">
500
- <div className="flex-1">
501
- <CardTitle className="text-base">{assistant.name}</CardTitle>
502
- <p className="text-sm text-muted-foreground mt-1">
503
- {assistant.description || 'No description'}
504
- </p>
505
- </div>
506
- <div className="flex gap-1">
507
- <Button
508
- variant="ghost"
509
- size="sm"
510
- onClick={onShare}
511
- className="text-blue-600 hover:text-blue-700"
512
- title="Share to community"
513
- >
514
- <Share className="h-4 w-4" />
515
- </Button>
516
- <Button
517
- variant="ghost"
518
- size="sm"
519
- onClick={onDelete}
520
- className="text-destructive hover:text-destructive"
521
- title="Delete assistant"
522
- >
523
- <Trash2 className="h-4 w-4" />
524
- </Button>
525
- </div>
526
- </div>
527
- </CardHeader>
528
- <CardContent className="space-y-3">
529
- <div className="text-xs space-y-1 text-muted-foreground">
530
- <div><span className="font-medium">Model:</span> {assistant.model}</div>
531
- <div><span className="font-medium">Temperature:</span> {assistant.temperature}</div>
532
- <div><span className="font-medium">Max Tokens:</span> {assistant.maxTokens}</div>
533
- <div><span className="font-medium">Created:</span> {new Date(assistant.createdAt).toLocaleDateString()}</div>
534
- </div>
535
- <Button size="sm" onClick={onUse} className="w-full">
536
- <MessageSquare className="h-4 w-4 mr-2" />
537
- Use Assistant
538
- </Button>
539
- </CardContent>
540
- </Card>
541
- )
542
- }
543
-
544
- // Community Template Card Component
545
- function CommunityTemplateCard({
546
- template,
547
- isLiked,
548
- onLike,
549
- onUse,
550
- featured = false
551
- }: {
552
- template: CommunityTemplate
553
- isLiked: boolean
554
- onLike: () => void
555
- onUse: () => void
556
- featured?: boolean
557
- }) {
558
- return (
559
- <Card className={`hover:shadow-md transition-shadow ${featured ? 'ring-2 ring-yellow-200' : ''}`}>
560
- <CardHeader className="pb-3">
561
- <div className="flex items-start justify-between">
562
- <div className="flex-1">
563
- <div className="flex items-center gap-2 mb-1">
564
- <CardTitle className="text-base">{template.name}</CardTitle>
565
- {template.isOfficial && (
566
- <Badge variant="default" className="text-xs px-2 py-0">
567
- Official
568
- </Badge>
569
- )}
570
- {featured && (
571
- <Star className="h-4 w-4 text-yellow-500 fill-current" />
572
- )}
573
- </div>
574
- <p className="text-xs text-muted-foreground">by {template.author}</p>
575
- <p className="text-sm text-muted-foreground mt-1 line-clamp-2">
576
- {template.description}
577
- </p>
578
- </div>
579
- <Button
580
- variant="ghost"
581
- size="sm"
582
- onClick={onLike}
583
- className={isLiked ? "text-red-500" : "text-muted-foreground hover:text-red-500"}
584
- title={isLiked ? "Unlike" : "Like"}
585
- >
586
- <Heart className={`h-4 w-4 ${isLiked ? 'fill-current' : ''}`} />
587
- </Button>
588
- </div>
589
- </CardHeader>
590
- <CardContent className="space-y-3">
591
- <div className="flex flex-wrap gap-1">
592
- {template.tags.slice(0, 3).map((tag) => (
593
- <Badge key={tag} variant="secondary" className="text-xs">
594
- {tag}
595
- </Badge>
596
- ))}
597
- </div>
598
-
599
- <div className="text-xs space-y-1 text-muted-foreground">
600
- <div><span className="font-medium">Model:</span> {template.model}</div>
601
- <div><span className="font-medium">Temperature:</span> {template.temperature}</div>
602
- <div><span className="font-medium">Max Tokens:</span> {template.maxTokens}</div>
603
- </div>
604
-
605
- <div className="flex items-center justify-between text-xs text-muted-foreground">
606
- <div className="flex items-center gap-3">
607
- <div className="flex items-center gap-1">
608
- <Heart className="h-3 w-3" />
609
- <span>{template.likes}</span>
610
- </div>
611
- <div className="flex items-center gap-1">
612
- <Download className="h-3 w-3" />
613
- <span>{template.downloads}</span>
614
- </div>
615
- </div>
616
- <span>{template.createdAt}</span>
617
- </div>
618
-
619
- <div className="flex gap-2">
620
- <Button size="sm" onClick={onUse} className="flex-1">
621
- <Copy className="h-4 w-4 mr-1" />
622
- Use Template
623
- </Button>
624
- </div>
625
- </CardContent>
626
- </Card>
627
- )
628
- }
629
-
630
  // ModelCard component for reusability
631
  interface ModelCardProps {
632
  model: ModelInfo
@@ -637,113 +255,102 @@ interface ModelCardProps {
637
 
638
  function ModelCard({ model, modelLoading, onLoad, onUnload }: ModelCardProps) {
639
  const isApiModel = model.type === 'api'
640
-
 
 
641
  return (
642
- <Card className="relative">
643
- <CardHeader>
644
  <div className="flex items-start justify-between">
645
- <div className="flex items-center gap-3">
646
- {isApiModel ? (
647
- <Cloud className="h-6 w-6 text-blue-500" />
648
- ) : model.supports_thinking ? (
649
- <Brain className="h-6 w-6 text-blue-500" />
650
- ) : (
651
- <Zap className="h-6 w-6 text-green-500" />
652
- )}
653
- <div>
654
- <CardTitle className="text-lg">{model.name}</CardTitle>
655
- <div className="flex items-center gap-2 mt-1 flex-wrap">
656
- {isApiModel ? (
657
- <Badge variant="default" className="bg-blue-600">
658
- <Cloud className="h-3 w-3 mr-1" />
659
- API Model
660
- </Badge>
661
- ) : (
662
- <Badge variant={model.supports_thinking ? "default" : "secondary"}>
663
- <HardDrive className="h-3 w-3 mr-1" />
664
- {model.supports_thinking ? "Thinking Model" : "Instruction Model"}
665
- </Badge>
666
- )}
667
- {model.is_loaded && (
668
- <Badge variant="outline" className="text-green-600 border-green-600">
669
- <CheckCircle className="h-3 w-3 mr-1" />
670
- {isApiModel ? "Ready" : "Loaded"}
671
- </Badge>
672
- )}
673
- </div>
674
- </div>
675
  </div>
 
676
  </div>
677
  </CardHeader>
678
- <CardContent className="space-y-4">
679
- <div>
680
- <p className="text-sm text-muted-foreground mb-2">{model.description}</p>
681
- <div className="flex items-center gap-4 text-xs text-muted-foreground">
682
- <span>Size: {model.size_gb}</span>
683
- {!isApiModel && <span>Format: Safetensors</span>}
684
- {isApiModel && <span>Type: Cloud API</span>}
685
- </div>
686
- </div>
687
-
688
- <div className="space-y-2">
689
- <h4 className="text-sm font-medium">Capabilities</h4>
690
- <div className="flex flex-wrap gap-2">
691
- <Badge variant="outline" className="text-xs">Text Generation</Badge>
692
- <Badge variant="outline" className="text-xs">Conversation</Badge>
693
- <Badge variant="outline" className="text-xs">Code</Badge>
694
- {model.supports_thinking && (
695
- <Badge variant="outline" className="text-xs">Reasoning</Badge>
696
- )}
697
- {isApiModel && model.model_name.includes('vl') && (
698
- <Badge variant="outline" className="text-xs">Vision</Badge>
699
- )}
 
 
 
 
700
  </div>
 
 
 
 
 
 
 
 
 
 
701
  </div>
702
 
703
- <div className="pt-2 border-t">
704
- {model.is_loaded ? (
705
- <div className="flex gap-2">
706
- {!isApiModel && (
707
- <Button
708
- variant="outline"
709
- size="sm"
710
- onClick={() => onUnload(model.model_name)}
711
- className="flex-1"
712
- >
713
- <Trash2 className="h-4 w-4 mr-2" />
714
- Unload
715
- </Button>
 
716
  )}
717
- <Button size="sm" className="flex-1" asChild>
718
- <a href="/playground">Use in Playground</a>
719
- </Button>
720
- </div>
721
  ) : (
722
  <Button
 
723
  onClick={() => onLoad(model.model_name)}
724
- disabled={modelLoading === model.model_name}
725
  className="w-full"
726
- size="sm"
727
  >
728
- {modelLoading === model.model_name ? (
729
- <>
730
- <Loader2 className="h-4 w-4 mr-2 animate-spin" />
731
- {isApiModel ? "Connecting..." : "Loading..."}
732
- </>
733
  ) : (
734
- <>
735
- {isApiModel ? (
736
- <Cloud className="h-4 w-4 mr-2" />
737
- ) : (
738
- <Download className="h-4 w-4 mr-2" />
739
- )}
740
- {isApiModel ? "Connect" : "Load Model"}
741
- </>
742
  )}
 
743
  </Button>
744
  )}
745
  </div>
746
  </CardContent>
747
  </Card>
748
  )
749
- }
 
2
  import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
3
  import { Button } from '@/components/ui/button'
4
  import { Badge } from '@/components/ui/badge'
 
5
  import {
6
  BookOpen,
7
  Brain,
 
12
  Info,
13
  CheckCircle,
14
  Cloud,
15
+ HardDrive
 
 
 
 
 
 
 
16
  } from 'lucide-react'
17
 
18
  interface ModelInfo {
19
  model_name: string
20
  name: string
21
  supports_thinking: boolean
22
+ ram_required_gb: string
23
  size_gb: string
24
  is_loaded: boolean
25
  type: 'local' | 'api'
 
30
  current_model: string
31
  }
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  export function Models() {
34
  const [models, setModels] = useState<ModelInfo[]>([])
35
  const [loading, setLoading] = useState(true)
36
  const [modelLoading, setModelLoading] = useState<string | null>(null)
 
 
 
37
 
38
  useEffect(() => {
39
  fetchModels()
 
 
40
  }, [])
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  const fetchModels = async () => {
43
  try {
44
  const baseUrl = `${window.location.protocol}//${window.location.host}`
45
  const res = await fetch(`${baseUrl}/models`)
46
+
47
+ if (!res.ok) {
48
+ throw new Error(`Failed to fetch models: ${res.status}`)
49
  }
50
+
51
+ const data: ModelsResponse = await res.json()
52
+ setModels(data.models)
53
+ } catch (error) {
54
+ console.error('Error fetching models:', error)
55
  } finally {
56
  setLoading(false)
57
  }
58
  }
59
 
60
+ const loadModel = async (modelName: string) => {
61
  setModelLoading(modelName)
62
  try {
63
  const baseUrl = `${window.location.protocol}//${window.location.host}`
64
  const res = await fetch(`${baseUrl}/load-model`, {
65
  method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ },
69
  body: JSON.stringify({ model_name: modelName }),
70
  })
71
+
72
+ if (!res.ok) {
73
+ throw new Error(`Failed to load model: ${res.status}`)
74
  }
75
+
76
+ // Refresh models list
77
+ fetchModels()
78
+ } catch (error) {
79
+ console.error('Error loading model:', error)
80
  } finally {
81
  setModelLoading(null)
82
  }
83
  }
84
 
85
+ const unloadModel = async (modelName: string) => {
86
+ setModelLoading(modelName)
87
  try {
88
  const baseUrl = `${window.location.protocol}//${window.location.host}`
89
  const res = await fetch(`${baseUrl}/unload-model`, {
90
  method: 'POST',
91
+ headers: {
92
+ 'Content-Type': 'application/json',
93
+ },
94
  body: JSON.stringify({ model_name: modelName }),
95
  })
96
+
97
+ if (!res.ok) {
98
+ throw new Error(`Failed to unload model: ${res.status}`)
99
  }
100
+
101
+ // Refresh models list
102
+ fetchModels()
103
+ } catch (error) {
104
+ console.error('Error unloading model:', error)
105
+ } finally {
106
+ setModelLoading(null)
107
  }
108
  }
109
 
110
  if (loading) {
111
  return (
112
+ <div className="min-h-screen bg-background flex items-center justify-center">
113
+ <Loader2 className="h-8 w-8 animate-spin" />
 
 
 
 
 
 
 
 
 
 
114
  </div>
115
  )
116
  }
 
118
  return (
119
  <div className="min-h-screen bg-background">
120
  {/* Header */}
121
+ <div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
122
+ <div className="max-w-6xl mx-auto p-6">
123
+ <div className="flex items-center justify-between">
124
+ <div className="flex items-center gap-3">
125
+ <div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
126
+ <BookOpen className="h-5 w-5 text-white" />
127
+ </div>
128
+ <div>
129
+ <h1 className="text-2xl font-bold">Model Catalog</h1>
130
+ <p className="text-sm text-muted-foreground">
131
+ Browse and manage AI models for your conversations
132
+ </p>
133
+ </div>
134
+ </div>
135
+ <Button
136
+ onClick={() => window.location.href = '/playground'}
137
+ className="flex items-center gap-2"
138
+ >
139
+ <Zap className="h-4 w-4" />
140
+ Go to Playground
141
  </Button>
142
  </div>
143
  </div>
144
  </div>
145
 
146
  <div className="flex-1 p-6">
147
+ <div className="max-w-6xl mx-auto space-y-6">
148
 
149
  {/* Info Card */}
150
+ <Card className="bg-blue-50 border-blue-200">
151
  <CardContent className="pt-6">
152
  <div className="flex items-start gap-3">
153
  <Info className="h-5 w-5 text-blue-600 mt-0.5" />
154
  <div>
155
+ <h3 className="font-medium text-blue-900">Model Management</h3>
156
  <p className="text-sm text-blue-700 mt-1">
157
+ Load models to use them in the playground. Models are cached locally for faster access.
158
+ Each model requires significant storage space and initial download time.
159
  </p>
160
  </div>
161
  </div>
162
  </CardContent>
163
  </Card>
164
 
165
+ {/* API Models Section */}
166
+ <div>
167
+ <h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
168
+ <Cloud className="h-5 w-5" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  API Models
170
  <Badge variant="outline" className="text-xs">Cloud-Powered</Badge>
171
  </h2>
172
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
173
  {models.filter(m => m.type === 'api').map((model) => (
174
  <ModelCard
175
+ key={model.model_name}
176
+ model={model}
177
  modelLoading={modelLoading}
178
+ onLoad={loadModel}
179
+ onUnload={unloadModel}
180
  />
181
  ))}
182
  </div>
 
187
  <h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
188
  <HardDrive className="h-5 w-5" />
189
  Local Models
190
+ <Badge variant="outline" className="text-xs">Self-Hosted</Badge>
191
  </h2>
192
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
193
  {models.filter(m => m.type === 'local').map((model) => (
194
  <ModelCard
195
+ key={model.model_name}
196
+ model={model}
197
  modelLoading={modelLoading}
198
+ onLoad={loadModel}
199
+ onUnload={unloadModel}
200
  />
201
  ))}
202
  </div>
203
  </div>
204
 
205
+ {/* Model Statistics */}
206
  <Card>
207
  <CardHeader>
208
+ <CardTitle className="flex items-center gap-2">
209
+ <Brain className="h-5 w-5" />
210
+ Model Statistics
211
+ </CardTitle>
212
  </CardHeader>
213
  <CardContent>
214
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-6">
215
  <div className="text-center">
216
+ <div className="text-2xl font-bold text-blue-600">
217
+ {models.filter(m => m.type === 'api').length}
218
+ </div>
219
+ <div className="text-sm text-muted-foreground">API Models</div>
220
  </div>
221
  <div className="text-center">
222
  <div className="text-2xl font-bold text-green-600">
223
+ {models.filter(m => m.type === 'local').length}
224
+ </div>
225
+ <div className="text-sm text-muted-foreground">Local Models</div>
226
+ </div>
227
+ <div className="text-center">
228
+ <div className="text-2xl font-bold text-orange-600">
229
  {models.filter(m => m.is_loaded).length}
230
  </div>
231
  <div className="text-sm text-muted-foreground">Loaded Models</div>
 
239
  </div>
240
  </CardContent>
241
  </Card>
 
 
242
  </div>
243
  </div>
244
  </div>
245
  )
246
  }
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  // ModelCard component for reusability
249
  interface ModelCardProps {
250
  model: ModelInfo
 
255
 
256
  function ModelCard({ model, modelLoading, onLoad, onUnload }: ModelCardProps) {
257
  const isApiModel = model.type === 'api'
258
+ const isLoading = modelLoading === model.model_name
259
+ const isLoaded = model.is_loaded
260
+
261
  return (
262
+ <Card className="hover:shadow-md transition-shadow">
263
+ <CardHeader className="pb-3">
264
  <div className="flex items-start justify-between">
265
+ <div className="flex-1">
266
+ <CardTitle className="text-base flex items-center gap-2">
267
+ {isApiModel ? (
268
+ <Cloud className="h-4 w-4 text-blue-600" />
269
+ ) : (
270
+ <HardDrive className="h-4 w-4 text-green-600" />
271
+ )}
272
+ {model.name}
273
+ </CardTitle>
274
+ <p className="text-xs text-muted-foreground mt-1">
275
+ {model.model_name}
276
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  </div>
278
+ {isLoaded && <CheckCircle className="h-5 w-5 text-green-600" />}
279
  </div>
280
  </CardHeader>
281
+
282
+ <CardContent className="space-y-3">
283
+ {/* Model Info */}
284
+ <div className="space-y-2 text-sm">
285
+ {!isApiModel && (
286
+ <>
287
+ <div className="flex justify-between">
288
+ <span className="text-muted-foreground">Size:</span>
289
+ <Badge variant="outline" className="text-xs">
290
+ {model.size_gb}
291
+ </Badge>
292
+ </div>
293
+ <div className="flex justify-between">
294
+ <span className="text-muted-foreground">RAM Required:</span>
295
+ <Badge variant="outline" className="text-xs">
296
+ {model.ram_required_gb}
297
+ </Badge>
298
+ </div>
299
+ </>
300
+ )}
301
+
302
+ <div className="flex justify-between">
303
+ <span className="text-muted-foreground">Type:</span>
304
+ <Badge variant={isApiModel ? "default" : "secondary"} className="text-xs">
305
+ {isApiModel ? 'API' : 'Local'}
306
+ </Badge>
307
  </div>
308
+
309
+ {model.supports_thinking && (
310
+ <div className="flex justify-between">
311
+ <span className="text-muted-foreground">Features:</span>
312
+ <Badge variant="outline" className="text-xs">
313
+ <Brain className="h-3 w-3 mr-1" />
314
+ Thinking
315
+ </Badge>
316
+ </div>
317
+ )}
318
  </div>
319
 
320
+ {/* Action Button */}
321
+ <div className="pt-2">
322
+ {isLoaded ? (
323
+ <Button
324
+ size="sm"
325
+ variant="outline"
326
+ onClick={() => onUnload(model.model_name)}
327
+ disabled={isLoading}
328
+ className="w-full"
329
+ >
330
+ {isLoading ? (
331
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
332
+ ) : (
333
+ <Trash2 className="h-4 w-4 mr-2" />
334
  )}
335
+ {isLoading ? 'Unloading...' : 'Unload Model'}
336
+ </Button>
 
 
337
  ) : (
338
  <Button
339
+ size="sm"
340
  onClick={() => onLoad(model.model_name)}
341
+ disabled={isLoading}
342
  className="w-full"
 
343
  >
344
+ {isLoading ? (
345
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
 
 
 
346
  ) : (
347
+ <Download className="h-4 w-4 mr-2" />
 
 
 
 
 
 
 
348
  )}
349
+ {isLoading ? 'Loading...' : 'Load Model'}
350
  </Button>
351
  )}
352
  </div>
353
  </CardContent>
354
  </Card>
355
  )
356
+ }