wu981526092 commited on
Commit
47f57e6
·
1 Parent(s): 9fa4261

Restructure user workflow: ChatGPT-style Assistant Builder + Pure Chat Interface

Browse files
frontend/src/App.tsx CHANGED
@@ -1,8 +1,8 @@
1
  import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
2
  import { Layout } from './components/Layout'
3
  import { Home } from './pages/Home'
4
- import { Playground } from './pages/PlaygroundRedirect'
5
- import { SimpleChat } from './pages/SimpleChat'
6
  import { AssistantBuilder } from './pages/AssistantBuilder'
7
  import { Models } from './pages/Models'
8
  import { Assistants } from './pages/Assistants'
@@ -16,7 +16,7 @@ function App() {
16
  <Route path="/" element={<Layout />}>
17
  <Route index element={<Home />} />
18
  <Route path="playground" element={<Playground />} />
19
- <Route path="chat" element={<SimpleChat />} />
20
  <Route path="assistant-builder" element={<AssistantBuilder />} />
21
  <Route path="models" element={<Models />} />
22
  <Route path="assistants" element={<Assistants />} />
 
1
  import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
2
  import { Layout } from './components/Layout'
3
  import { Home } from './pages/Home'
4
+ import { Playground } from './pages/Playground'
5
+ import { ChatInterface } from './pages/ChatInterface'
6
  import { AssistantBuilder } from './pages/AssistantBuilder'
7
  import { Models } from './pages/Models'
8
  import { Assistants } from './pages/Assistants'
 
16
  <Route path="/" element={<Layout />}>
17
  <Route index element={<Home />} />
18
  <Route path="playground" element={<Playground />} />
19
+ <Route path="chat" element={<ChatInterface />} />
20
  <Route path="assistant-builder" element={<AssistantBuilder />} />
21
  <Route path="models" element={<Models />} />
22
  <Route path="assistants" element={<Assistants />} />
frontend/src/components/Sidebar.tsx CHANGED
@@ -7,7 +7,8 @@ import {
7
  Settings,
8
  Brain,
9
  Bot,
10
- Users
 
11
  } from 'lucide-react'
12
 
13
  const navigation = [
@@ -18,33 +19,33 @@ const navigation = [
18
  description: 'Overview and getting started'
19
  },
20
  {
21
- name: 'Quick Chat',
22
  href: '/chat',
23
  icon: MessageSquare,
24
- description: 'Simple AI conversations'
25
  },
26
  {
27
  name: 'Assistant Builder',
28
  href: '/assistant-builder',
29
- icon: Bot,
30
- description: 'Create custom AI assistants'
31
  },
32
  {
33
  name: 'My Assistants',
34
  href: '/assistants',
35
- icon: Users,
36
  description: 'Manage your saved assistants'
37
  },
38
  {
39
  name: 'Community',
40
  href: '/community',
41
- icon: BookOpen,
42
- description: 'Discover assistant templates'
43
  },
44
  {
45
  name: 'Models',
46
  href: '/models',
47
- icon: Brain,
48
  description: 'Manage AI models'
49
  },
50
  {
@@ -77,7 +78,7 @@ export function Sidebar() {
77
  <div className="flex-1 px-3 py-4 space-y-8">
78
  <div>
79
  <h2 className="mb-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
80
- Features
81
  </h2>
82
  <nav className="space-y-1">
83
  {navigation.map((item) => {
 
7
  Settings,
8
  Brain,
9
  Bot,
10
+ Users,
11
+ Wrench
12
  } from 'lucide-react'
13
 
14
  const navigation = [
 
19
  description: 'Overview and getting started'
20
  },
21
  {
22
+ name: 'Chat',
23
  href: '/chat',
24
  icon: MessageSquare,
25
+ description: 'Chat with your AI assistants'
26
  },
27
  {
28
  name: 'Assistant Builder',
29
  href: '/assistant-builder',
30
+ icon: Wrench,
31
+ description: 'Create and customize AI assistants'
32
  },
33
  {
34
  name: 'My Assistants',
35
  href: '/assistants',
36
+ icon: Bot,
37
  description: 'Manage your saved assistants'
38
  },
39
  {
40
  name: 'Community',
41
  href: '/community',
42
+ icon: Users,
43
+ description: 'Discover community templates'
44
  },
45
  {
46
  name: 'Models',
47
  href: '/models',
48
+ icon: BookOpen,
49
  description: 'Manage AI models'
50
  },
51
  {
 
78
  <div className="flex-1 px-3 py-4 space-y-8">
79
  <div>
80
  <h2 className="mb-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
81
+ Get started
82
  </h2>
83
  <nav className="space-y-1">
84
  {navigation.map((item) => {
frontend/src/pages/AssistantBuilder.tsx CHANGED
@@ -1,114 +1,230 @@
1
- import { useState } from 'react'
2
  import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
3
  import { Button } from '@/components/ui/button'
 
4
  import { Input } from '@/components/ui/input'
5
  import { Textarea } from '@/components/ui/textarea'
6
- import { Badge } from '@/components/ui/badge'
 
 
 
 
 
 
 
 
7
  import {
8
  Bot,
 
9
  MessageSquare,
10
  Settings,
11
- Save,
 
12
  ArrowLeft,
13
- Send,
14
- User
 
 
15
  } from 'lucide-react'
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  export function AssistantBuilder() {
18
- const [assistantConfig, setAssistantConfig] = useState({
 
 
 
 
19
  name: '',
20
  description: '',
21
  instructions: '',
22
- model: 'Qwen/Qwen3-30B-A3B',
23
  temperature: 0.7,
24
- maxTokens: 2000,
25
- conversation_starters: ['', '', '', '']
26
  })
27
 
28
- const [testMessages, setTestMessages] = useState<Array<{role: 'user' | 'assistant', content: string}>>([])
29
- const [testInput, setTestInput] = useState('')
30
- const [isTesting, setIsTesting] = useState(false)
31
 
32
- const updateConfig = (field: string, value: any) => {
33
- setAssistantConfig(prev => ({ ...prev, [field]: value }))
34
- }
 
 
35
 
36
- const updateConversationStarter = (index: number, value: string) => {
37
- const starters = [...assistantConfig.conversation_starters]
38
- starters[index] = value
39
- updateConfig('conversation_starters', starters)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
 
42
- const testAssistant = async () => {
43
- if (!testInput.trim()) return
 
 
 
 
 
 
 
 
 
 
 
44
 
45
- const userMessage = { role: 'user' as const, content: testInput }
46
- setTestMessages(prev => [...prev, userMessage])
47
- setTestInput('')
48
- setIsTesting(true)
 
49
 
 
 
50
  try {
51
- const baseUrl = `${window.location.protocol}//${window.location.host}`
52
- const messages = [...testMessages, userMessage]
 
 
 
 
53
 
54
- const response = await fetch(`${baseUrl}/chat/completions`, {
55
- method: 'POST',
56
- headers: { 'Content-Type': 'application/json' },
57
- body: JSON.stringify({
58
- messages: [
59
- { role: 'system', content: assistantConfig.instructions },
60
- ...messages
61
- ],
62
- temperature: assistantConfig.temperature,
63
- max_tokens: assistantConfig.maxTokens
64
- })
65
- })
66
-
67
- if (response.ok) {
68
- const data = await response.json()
69
- const assistantMessage = {
70
- role: 'assistant' as const,
71
- content: data.choices[0]?.message?.content || 'No response'
72
  }
73
- setTestMessages(prev => [...prev, assistantMessage])
 
74
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  } catch (error) {
76
- console.error('Test failed:', error)
 
77
  } finally {
78
- setIsTesting(false)
79
  }
80
  }
81
 
82
- const saveAssistant = () => {
83
- const assistant = {
84
- id: `assistant_${Date.now()}`,
85
- ...assistantConfig,
86
- createdAt: new Date().toISOString(),
87
- systemPrompt: assistantConfig.instructions
88
  }
89
 
90
- try {
91
- const saved = JSON.parse(localStorage.getItem('savedAssistants') || '[]')
92
- saved.push(assistant)
93
- localStorage.setItem('savedAssistants', JSON.stringify(saved))
94
-
95
- alert('Assistant saved successfully!')
96
- window.location.href = '/assistants'
97
- } catch (error) {
98
- console.error('Failed to save assistant:', error)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
 
 
 
 
 
 
 
 
 
 
 
100
  }
101
 
102
  return (
103
- <div className="min-h-screen bg-gray-50">
104
  {/* Header */}
105
- <div className="bg-white border-b border-gray-200">
106
- <div className="max-w-7xl mx-auto px-6 py-4">
107
  <div className="flex items-center justify-between">
108
  <div className="flex items-center gap-4">
109
  <Button
110
  variant="ghost"
111
- size="sm"
112
  onClick={() => window.location.href = '/assistants'}
113
  className="flex items-center gap-2"
114
  >
@@ -120,222 +236,239 @@ export function AssistantBuilder() {
120
  <Bot className="h-5 w-5 text-white" />
121
  </div>
122
  <div>
123
- <h1 className="text-xl font-semibold">Assistant Builder</h1>
124
- <p className="text-sm text-gray-500">Create your custom AI assistant</p>
 
 
 
 
125
  </div>
126
  </div>
127
  </div>
 
128
  <div className="flex items-center gap-3">
129
- <Button variant="outline" onClick={() => setTestMessages([])}>
130
- Clear Test
 
 
 
 
 
131
  </Button>
132
- <Button onClick={saveAssistant} className="flex items-center gap-2">
133
- <Save className="h-4 w-4" />
134
- Save Assistant
 
 
 
 
 
 
 
 
 
 
 
 
135
  </Button>
136
  </div>
137
  </div>
138
  </div>
139
  </div>
140
 
141
- <div className="max-w-7xl mx-auto p-6">
142
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 h-[calc(100vh-160px)]">
 
 
143
 
144
- {/* Configuration Panel */}
145
- <div className="space-y-6">
146
- <Card>
147
- <CardHeader>
148
- <CardTitle className="flex items-center gap-2">
149
- <Settings className="h-5 w-5" />
150
- Configure Assistant
151
- </CardTitle>
152
- </CardHeader>
153
- <CardContent className="space-y-6">
154
- {/* Basic Info */}
155
- <div className="space-y-4">
156
- <div>
157
- <label className="text-sm font-medium mb-2 block">Name</label>
158
- <Input
159
- placeholder="e.g., Marketing Expert"
160
- value={assistantConfig.name}
161
- onChange={(e) => updateConfig('name', e.target.value)}
162
- />
163
- </div>
164
-
165
- <div>
166
- <label className="text-sm font-medium mb-2 block">Description</label>
167
- <Input
168
- placeholder="What does this assistant do?"
169
- value={assistantConfig.description}
170
- onChange={(e) => updateConfig('description', e.target.value)}
171
- />
172
- </div>
173
- </div>
174
-
175
- {/* Instructions - Much Larger */}
176
- <div>
177
- <label className="text-sm font-medium mb-2 block">Instructions</label>
178
- <p className="text-xs text-gray-500 mb-2">
179
- Define your assistant's behavior, expertise, and communication style
180
- </p>
181
- <Textarea
182
- className="min-h-[200px] font-mono text-sm"
183
- placeholder={`You are a helpful marketing expert assistant. You should:
184
-
185
- 1. Provide actionable marketing advice
186
- 2. Use data-driven insights when possible
187
- 3. Explain complex concepts simply
188
- 4. Ask clarifying questions to give better recommendations
189
- 5. Stay up-to-date with current marketing trends
190
-
191
- Your responses should be professional yet approachable, and always include specific examples when helpful.`}
192
- value={assistantConfig.instructions}
193
- onChange={(e) => updateConfig('instructions', e.target.value)}
194
- />
195
- </div>
196
-
197
- {/* Model Settings */}
198
- <div className="grid grid-cols-2 gap-4">
199
- <div>
200
- <label className="text-sm font-medium mb-2 block">Temperature</label>
201
- <Input
202
- type="number"
203
- min="0"
204
- max="2"
205
- step="0.1"
206
- value={assistantConfig.temperature}
207
- onChange={(e) => updateConfig('temperature', parseFloat(e.target.value))}
208
- />
209
- </div>
210
- <div>
211
- <label className="text-sm font-medium mb-2 block">Max Tokens</label>
212
- <Input
213
- type="number"
214
- min="100"
215
- max="4000"
216
- step="100"
217
- value={assistantConfig.maxTokens}
218
- onChange={(e) => updateConfig('maxTokens', parseInt(e.target.value))}
219
- />
220
- </div>
221
- </div>
222
 
223
- {/* Conversation Starters */}
224
- <div>
225
- <label className="text-sm font-medium mb-2 block">Conversation Starters</label>
226
- <p className="text-xs text-gray-500 mb-2">
227
- Suggested prompts to help users get started
228
- </p>
229
- <div className="space-y-2">
230
- {assistantConfig.conversation_starters.map((starter, index) => (
231
- <Input
232
- key={index}
233
- placeholder={`Conversation starter ${index + 1}`}
234
- value={starter}
235
- onChange={(e) => updateConversationStarter(index, e.target.value)}
236
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  ))}
238
- </div>
239
- </div>
240
- </CardContent>
241
- </Card>
242
- </div>
243
-
244
- {/* Test Panel */}
245
- <div className="flex flex-col">
246
- <Card className="flex-1 flex flex-col">
247
- <CardHeader className="pb-3">
248
- <CardTitle className="flex items-center gap-2">
249
- <MessageSquare className="h-5 w-5" />
250
- Test Your Assistant
251
- {assistantConfig.name && (
252
- <Badge variant="outline" className="ml-auto">
253
- {assistantConfig.name}
254
- </Badge>
255
- )}
256
- </CardTitle>
257
- </CardHeader>
258
 
259
- {/* Messages Area */}
260
- <CardContent className="flex-1 flex flex-col">
261
- <div className="flex-1 bg-gray-50 rounded-lg p-4 mb-4 overflow-y-auto space-y-4 min-h-[300px]">
262
- {testMessages.length === 0 ? (
263
- <div className="flex flex-col items-center justify-center h-full text-center text-gray-500">
264
- <Bot className="h-12 w-12 mb-4 text-gray-300" />
265
- <p className="text-sm">Start testing your assistant</p>
266
- <p className="text-xs text-gray-400 mt-1">Messages will appear here</p>
267
- </div>
268
- ) : (
269
- testMessages.map((msg, index) => (
270
- <div key={index} className={`flex gap-3 ${msg.role === 'assistant' ? 'justify-start' : 'justify-end'}`}>
271
- {msg.role === 'assistant' && (
272
- <div className="w-8 h-8 bg-purple-600 rounded-full flex items-center justify-center">
273
- <Bot className="h-4 w-4 text-white" />
274
- </div>
275
- )}
276
- <div className={`max-w-[80%] p-3 rounded-lg ${
277
- msg.role === 'assistant'
278
- ? 'bg-white border border-gray-200'
279
- : 'bg-blue-600 text-white'
280
- }`}>
281
- <p className="text-sm whitespace-pre-wrap">{msg.content}</p>
282
- </div>
283
- {msg.role === 'user' && (
284
- <div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
285
- <User className="h-4 w-4 text-white" />
286
- </div>
287
- )}
288
- </div>
289
- ))
290
- )}
291
- </div>
 
 
 
 
 
292
 
293
- {/* Input Area */}
294
- <div className="flex gap-2">
295
- <Input
296
- placeholder="Test your assistant..."
297
- value={testInput}
298
- onChange={(e) => setTestInput(e.target.value)}
299
- onKeyPress={(e) => e.key === 'Enter' && !e.shiftKey && testAssistant()}
300
- disabled={isTesting}
301
- />
302
- <Button
303
- onClick={testAssistant}
304
- disabled={isTesting || !testInput.trim()}
305
- size="sm"
306
- >
307
- {isTesting ? (
308
- <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
309
- ) : (
310
- <Send className="h-4 w-4" />
311
- )}
312
- </Button>
313
- </div>
 
 
 
 
 
 
 
 
314
 
315
- {/* Conversation Starters in Test Area */}
316
- {assistantConfig.conversation_starters.some(s => s.trim()) && (
317
- <div className="mt-3 pt-3 border-t border-gray-200">
318
- <p className="text-xs text-gray-500 mb-2">Try these:</p>
319
- <div className="flex flex-wrap gap-2">
320
- {assistantConfig.conversation_starters
321
- .filter(s => s.trim())
322
- .map((starter, index) => (
323
- <Button
324
- key={index}
325
- variant="outline"
326
- size="sm"
327
- className="text-xs h-auto py-1 px-2"
328
- onClick={() => setTestInput(starter)}
329
- >
330
- {starter}
331
- </Button>
332
- ))}
 
 
 
 
 
 
 
 
 
 
 
333
  </div>
334
  </div>
335
- )}
336
- </CardContent>
337
- </Card>
338
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  </div>
340
  </div>
341
  </div>
 
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 { Input } from '@/components/ui/input'
6
  import { Textarea } from '@/components/ui/textarea'
7
+ import { Slider } from '@/components/ui/slider'
8
+ import { Label } from '@/components/ui/label'
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from '@/components/ui/select'
16
  import {
17
  Bot,
18
+ Save,
19
  MessageSquare,
20
  Settings,
21
+ Brain,
22
+ Zap,
23
  ArrowLeft,
24
+ Copy,
25
+ Sparkles,
26
+ User,
27
+ Info
28
  } from 'lucide-react'
29
 
30
+ interface ModelInfo {
31
+ model_name: string
32
+ name: string
33
+ supports_thinking: boolean
34
+ description: string
35
+ size_gb: string
36
+ is_loaded: boolean
37
+ type: 'local' | 'api'
38
+ }
39
+
40
+ interface ModelsResponse {
41
+ models: ModelInfo[]
42
+ current_model: string
43
+ }
44
+
45
+ interface AssistantConfig {
46
+ id?: string
47
+ name: string
48
+ description: string
49
+ instructions: string
50
+ model: string
51
+ temperature: number
52
+ maxTokens: number
53
+ capabilities: string[]
54
+ createdAt?: string
55
+ }
56
+
57
  export function AssistantBuilder() {
58
+ const [models, setModels] = useState<ModelInfo[]>([])
59
+ const [loading, setLoading] = useState(true)
60
+
61
+ // Assistant configuration
62
+ const [config, setConfig] = useState<AssistantConfig>({
63
  name: '',
64
  description: '',
65
  instructions: '',
66
+ model: '',
67
  temperature: 0.7,
68
+ maxTokens: 1024,
69
+ capabilities: []
70
  })
71
 
72
+ // UI state
73
+ const [saving, setSaving] = useState(false)
74
+ const [isEditing, setIsEditing] = useState(false)
75
 
76
+ useEffect(() => {
77
+ fetchModels()
78
+ // Check if we're editing an existing assistant
79
+ checkForExistingAssistant()
80
+ }, [])
81
 
82
+ const fetchModels = async () => {
83
+ try {
84
+ const baseUrl = `${window.location.protocol}//${window.location.host}`
85
+ const res = await fetch(`${baseUrl}/models`)
86
+
87
+ if (!res.ok) {
88
+ throw new Error(`Failed to fetch models: ${res.status}`)
89
+ }
90
+
91
+ const data: ModelsResponse = await res.json()
92
+ setModels(data.models)
93
+
94
+ // Set default model if none selected
95
+ if (!config.model && data.models.length > 0) {
96
+ const apiModels = data.models.filter(m => m.type === 'api')
97
+ const defaultModel = apiModels.length > 0 ? apiModels[0].model_name : data.models[0].model_name
98
+ setConfig(prev => ({ ...prev, model: defaultModel }))
99
+ }
100
+ } catch (error) {
101
+ console.error('Error fetching models:', error)
102
+ } finally {
103
+ setLoading(false)
104
+ }
105
  }
106
 
107
+ const checkForExistingAssistant = () => {
108
+ const editConfig = localStorage.getItem('editAssistantConfig')
109
+ if (editConfig) {
110
+ try {
111
+ const existingConfig = JSON.parse(editConfig)
112
+ setConfig(existingConfig)
113
+ setIsEditing(true)
114
+ localStorage.removeItem('editAssistantConfig')
115
+ } catch (error) {
116
+ console.error('Failed to load existing assistant config:', error)
117
+ }
118
+ }
119
+ }
120
 
121
+ const saveAssistant = async () => {
122
+ if (!config.name.trim() || !config.instructions.trim()) {
123
+ alert('Please provide a name and instructions for your assistant.')
124
+ return
125
+ }
126
 
127
+ setSaving(true)
128
+
129
  try {
130
+ const assistant = {
131
+ ...config,
132
+ id: config.id || Date.now().toString(),
133
+ createdAt: config.createdAt || new Date().toISOString(),
134
+ updatedAt: new Date().toISOString()
135
+ }
136
 
137
+ // Save to localStorage
138
+ const savedAssistants = JSON.parse(localStorage.getItem('savedAssistants') || '[]')
139
+
140
+ if (isEditing && config.id) {
141
+ const index = savedAssistants.findIndex((a: any) => a.id === config.id)
142
+ if (index !== -1) {
143
+ savedAssistants[index] = assistant
 
 
 
 
 
 
 
 
 
 
 
144
  }
145
+ } else {
146
+ savedAssistants.push(assistant)
147
  }
148
+
149
+ localStorage.setItem('savedAssistants', JSON.stringify(savedAssistants))
150
+
151
+ alert(`Assistant "${config.name}" saved successfully!`)
152
+
153
+ // Reset form or navigate to assistants page
154
+ setConfig({
155
+ name: '',
156
+ description: '',
157
+ instructions: '',
158
+ model: models.length > 0 ? models[0].model_name : '',
159
+ temperature: 0.7,
160
+ maxTokens: 1024,
161
+ capabilities: []
162
+ })
163
+ setIsEditing(false)
164
+
165
  } catch (error) {
166
+ console.error('Error saving assistant:', error)
167
+ alert('Failed to save assistant. Please try again.')
168
  } finally {
169
+ setSaving(false)
170
  }
171
  }
172
 
173
+ const testAssistant = () => {
174
+ if (!config.name.trim() || !config.instructions.trim()) {
175
+ alert('Please provide a name and instructions before testing.')
176
+ return
 
 
177
  }
178
 
179
+ // Store current config for playground to use
180
+ localStorage.setItem('testAssistantConfig', JSON.stringify({
181
+ ...config,
182
+ systemPrompt: config.instructions
183
+ }))
184
+
185
+ // Navigate to playground
186
+ window.location.href = '/playground'
187
+ }
188
+
189
+ const systemPromptExamples = [
190
+ {
191
+ title: "Code Review Expert",
192
+ description: "Analyzes code for quality, security, and best practices",
193
+ prompt: "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."
194
+ },
195
+ {
196
+ title: "Writing Assistant",
197
+ description: "Helps improve academic and professional writing",
198
+ prompt: "You are an experienced 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. **Style**: Appropriate tone, engaging voice\n4. **Grammar & Mechanics**: Correct errors, improve sentence variety\n\nProvide specific feedback with examples and rewrite suggestions where helpful."
199
+ },
200
+ {
201
+ title: "Creative Brainstorming Partner",
202
+ description: "Generates creative ideas and helps develop concepts",
203
+ prompt: "You are a creative brainstorming partner with expertise in idea generation. Help users by:\n\n1. **Idea Generation**: Provide diverse, creative, and original ideas\n2. **Concept Development**: Help refine and expand on initial concepts\n3. **Problem Solving**: Approach challenges from multiple angles\n4. **Innovation**: Suggest unconventional solutions and approaches\n\nBe enthusiastic, encouraging, and think outside the box."
204
  }
205
+ ]
206
+
207
+ if (loading) {
208
+ return (
209
+ <div className="min-h-screen bg-background flex items-center justify-center">
210
+ <div className="text-center">
211
+ <Brain className="h-8 w-8 animate-pulse mx-auto mb-4 text-blue-600" />
212
+ <p>Loading models...</p>
213
+ </div>
214
+ </div>
215
+ )
216
  }
217
 
218
  return (
219
+ <div className="min-h-screen bg-background">
220
  {/* Header */}
221
+ <div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
222
+ <div className="max-w-6xl mx-auto p-6">
223
  <div className="flex items-center justify-between">
224
  <div className="flex items-center gap-4">
225
  <Button
226
  variant="ghost"
227
+ size="sm"
228
  onClick={() => window.location.href = '/assistants'}
229
  className="flex items-center gap-2"
230
  >
 
236
  <Bot className="h-5 w-5 text-white" />
237
  </div>
238
  <div>
239
+ <h1 className="text-2xl font-bold">
240
+ {isEditing ? 'Edit Assistant' : 'Create Assistant'}
241
+ </h1>
242
+ <p className="text-sm text-muted-foreground">
243
+ Design a custom AI assistant with specialized capabilities
244
+ </p>
245
  </div>
246
  </div>
247
  </div>
248
+
249
  <div className="flex items-center gap-3">
250
+ <Button
251
+ variant="outline"
252
+ onClick={testAssistant}
253
+ disabled={!config.name.trim() || !config.instructions.trim()}
254
+ >
255
+ <MessageSquare className="h-4 w-4 mr-2" />
256
+ Test Chat
257
  </Button>
258
+ <Button
259
+ onClick={saveAssistant}
260
+ disabled={saving || !config.name.trim() || !config.instructions.trim()}
261
+ >
262
+ {saving ? (
263
+ <>
264
+ <Zap className="h-4 w-4 mr-2 animate-spin" />
265
+ Saving...
266
+ </>
267
+ ) : (
268
+ <>
269
+ <Save className="h-4 w-4 mr-2" />
270
+ Save Assistant
271
+ </>
272
+ )}
273
  </Button>
274
  </div>
275
  </div>
276
  </div>
277
  </div>
278
 
279
+ <div className="max-w-6xl mx-auto p-6 grid grid-cols-1 lg:grid-cols-2 gap-8">
280
+
281
+ {/* Configuration Panel */}
282
+ <div className="space-y-6">
283
 
284
+ {/* Basic Information */}
285
+ <Card>
286
+ <CardHeader>
287
+ <CardTitle className="flex items-center gap-2">
288
+ <User className="h-5 w-5" />
289
+ Basic Information
290
+ </CardTitle>
291
+ </CardHeader>
292
+ <CardContent className="space-y-4">
293
+ <div>
294
+ <Label htmlFor="name">Assistant Name</Label>
295
+ <Input
296
+ id="name"
297
+ value={config.name}
298
+ onChange={(e) => setConfig(prev => ({ ...prev, name: e.target.value }))}
299
+ placeholder="e.g., Code Review Expert, Writing Assistant"
300
+ className="mt-1"
301
+ />
302
+ </div>
303
+
304
+ <div>
305
+ <Label htmlFor="description">Description</Label>
306
+ <Input
307
+ id="description"
308
+ value={config.description}
309
+ onChange={(e) => setConfig(prev => ({ ...prev, description: e.target.value }))}
310
+ placeholder="Brief description of what this assistant does"
311
+ className="mt-1"
312
+ />
313
+ </div>
314
+ </CardContent>
315
+ </Card>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
 
317
+ {/* Model Configuration */}
318
+ <Card>
319
+ <CardHeader>
320
+ <CardTitle className="flex items-center gap-2">
321
+ <Brain className="h-5 w-5" />
322
+ Model & Parameters
323
+ </CardTitle>
324
+ </CardHeader>
325
+ <CardContent className="space-y-4">
326
+ <div>
327
+ <Label htmlFor="model">AI Model</Label>
328
+ <Select
329
+ value={config.model}
330
+ onValueChange={(value) => setConfig(prev => ({ ...prev, model: value }))}
331
+ >
332
+ <SelectTrigger className="mt-1">
333
+ <SelectValue placeholder="Choose a model" />
334
+ </SelectTrigger>
335
+ <SelectContent>
336
+ {models.map((model) => (
337
+ <SelectItem key={model.model_name} value={model.model_name}>
338
+ <div className="flex items-center gap-2">
339
+ <Badge variant={model.type === 'api' ? 'default' : 'secondary'} className="text-xs">
340
+ {model.type === 'api' ? 'API' : 'Local'}
341
+ </Badge>
342
+ {model.name}
343
+ </div>
344
+ </SelectItem>
345
  ))}
346
+ </SelectContent>
347
+ </Select>
348
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
+ <div>
351
+ <Label htmlFor="temperature">
352
+ Temperature: {config.temperature}
353
+ </Label>
354
+ <Slider
355
+ id="temperature"
356
+ min={0}
357
+ max={1}
358
+ step={0.1}
359
+ value={[config.temperature]}
360
+ onValueChange={([value]) => setConfig(prev => ({ ...prev, temperature: value }))}
361
+ className="mt-2"
362
+ />
363
+ <p className="text-xs text-muted-foreground mt-1">
364
+ Lower = more focused, Higher = more creative
365
+ </p>
366
+ </div>
367
+
368
+ <div>
369
+ <Label htmlFor="maxTokens">
370
+ Max Tokens: {config.maxTokens}
371
+ </Label>
372
+ <Slider
373
+ id="maxTokens"
374
+ min={128}
375
+ max={4096}
376
+ step={128}
377
+ value={[config.maxTokens]}
378
+ onValueChange={([value]) => setConfig(prev => ({ ...prev, maxTokens: value }))}
379
+ className="mt-2"
380
+ />
381
+ <p className="text-xs text-muted-foreground mt-1">
382
+ Maximum length of the response
383
+ </p>
384
+ </div>
385
+ </CardContent>
386
+ </Card>
387
+ </div>
388
 
389
+ {/* Instructions Panel */}
390
+ <div className="space-y-6">
391
+
392
+ {/* System Instructions */}
393
+ <Card>
394
+ <CardHeader>
395
+ <CardTitle className="flex items-center gap-2">
396
+ <Settings className="h-5 w-5" />
397
+ Instructions
398
+ </CardTitle>
399
+ </CardHeader>
400
+ <CardContent>
401
+ <div>
402
+ <Label htmlFor="instructions">
403
+ System Instructions
404
+ </Label>
405
+ <Textarea
406
+ id="instructions"
407
+ value={config.instructions}
408
+ onChange={(e) => setConfig(prev => ({ ...prev, instructions: e.target.value }))}
409
+ placeholder="Define how your assistant should behave, its role, and capabilities..."
410
+ className="mt-1 min-h-[300px] text-sm"
411
+ />
412
+ <p className="text-xs text-muted-foreground mt-2">
413
+ These instructions define your assistant's behavior, expertise, and response style.
414
+ </p>
415
+ </div>
416
+ </CardContent>
417
+ </Card>
418
 
419
+ {/* Examples */}
420
+ <Card>
421
+ <CardHeader>
422
+ <CardTitle className="flex items-center gap-2">
423
+ <Sparkles className="h-5 w-5" />
424
+ Example Templates
425
+ </CardTitle>
426
+ </CardHeader>
427
+ <CardContent>
428
+ <div className="space-y-3">
429
+ {systemPromptExamples.map((example) => (
430
+ <div
431
+ key={example.title}
432
+ className="p-3 border rounded-lg hover:bg-gray-50 cursor-pointer transition-colors"
433
+ onClick={() => setConfig(prev => ({
434
+ ...prev,
435
+ instructions: example.prompt,
436
+ name: prev.name || example.title,
437
+ description: prev.description || example.description
438
+ }))}
439
+ >
440
+ <div className="flex items-start justify-between">
441
+ <div>
442
+ <h4 className="font-medium text-sm">{example.title}</h4>
443
+ <p className="text-xs text-muted-foreground mt-1">
444
+ {example.description}
445
+ </p>
446
+ </div>
447
+ <Copy className="h-4 w-4 text-muted-foreground" />
448
  </div>
449
  </div>
450
+ ))}
451
+ </div>
452
+ </CardContent>
453
+ </Card>
454
+
455
+ {/* Info */}
456
+ <Card className="bg-blue-50 border-blue-200">
457
+ <CardContent className="pt-6">
458
+ <div className="flex items-start gap-3">
459
+ <Info className="h-5 w-5 text-blue-600 mt-0.5" />
460
+ <div>
461
+ <h3 className="font-medium text-blue-900">Tips for Great Instructions</h3>
462
+ <ul className="text-sm text-blue-700 mt-2 space-y-1">
463
+ <li>• Be specific about the assistant's role and expertise</li>
464
+ <li>• Include examples of desired behavior</li>
465
+ <li>• Define the tone and style of responses</li>
466
+ <li>• Specify any constraints or guidelines</li>
467
+ </ul>
468
+ </div>
469
+ </div>
470
+ </CardContent>
471
+ </Card>
472
  </div>
473
  </div>
474
  </div>
frontend/src/pages/ChatInterface.tsx ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+ import { Button } from '@/components/ui/button'
3
+ import { Card, CardContent } from '@/components/ui/card'
4
+ import { Badge } from '@/components/ui/badge'
5
+
6
+ import { Chat } from '@/components/ui/chat'
7
+ import { useChat } from '@/hooks/useChat'
8
+ import {
9
+ MessageSquare,
10
+ Settings,
11
+ Bot,
12
+ Plus,
13
+ Trash2,
14
+ History,
15
+ PanelLeftOpen,
16
+ PanelLeftClose
17
+ } from 'lucide-react'
18
+
19
+ interface AssistantConfig {
20
+ id: string
21
+ name: string
22
+ description: string
23
+ instructions: string
24
+ model: string
25
+ temperature: number
26
+ maxTokens: number
27
+ capabilities: string[]
28
+ createdAt: string
29
+ }
30
+
31
+ export function ChatInterface() {
32
+ const [savedAssistants, setSavedAssistants] = useState<AssistantConfig[]>([])
33
+ const [selectedAssistant, setSelectedAssistant] = useState<AssistantConfig | null>(null)
34
+ const [showSessions, setShowSessions] = useState(false)
35
+
36
+ // Chat functionality with restricted access
37
+ const {
38
+ sessions,
39
+ currentSessionId,
40
+ createNewSession,
41
+ selectSession,
42
+ deleteSession,
43
+ messages,
44
+ input,
45
+ setInput,
46
+ sendMessage,
47
+ stopGeneration,
48
+ isLoading,
49
+ // Remove direct access to these - they should be controlled by assistant
50
+ setSelectedModel,
51
+ setSystemPrompt,
52
+ setTemperature,
53
+ setMaxTokens
54
+ } = useChat()
55
+
56
+ useEffect(() => {
57
+ loadSavedAssistants()
58
+
59
+ // Check if we're loading a specific assistant
60
+ checkForAssistantToLoad()
61
+ }, [])
62
+
63
+ const loadSavedAssistants = () => {
64
+ try {
65
+ const assistants = JSON.parse(localStorage.getItem('savedAssistants') || '[]')
66
+ setSavedAssistants(assistants)
67
+
68
+ // If no assistant selected and we have assistants, select the first one
69
+ if (!selectedAssistant && assistants.length > 0) {
70
+ loadAssistant(assistants[0])
71
+ }
72
+ } catch (error) {
73
+ console.error('Failed to load assistants:', error)
74
+ }
75
+ }
76
+
77
+ const checkForAssistantToLoad = () => {
78
+ const testConfig = localStorage.getItem('testAssistantConfig')
79
+ const loadConfig = localStorage.getItem('loadAssistantConfig')
80
+
81
+ if (testConfig) {
82
+ try {
83
+ const config = JSON.parse(testConfig)
84
+ setSelectedAssistant(config)
85
+ applyAssistantConfig(config)
86
+ localStorage.removeItem('testAssistantConfig')
87
+ } catch (error) {
88
+ console.error('Failed to load test assistant config:', error)
89
+ }
90
+ } else if (loadConfig) {
91
+ try {
92
+ const config = JSON.parse(loadConfig)
93
+ setSelectedAssistant(config)
94
+ applyAssistantConfig(config)
95
+ localStorage.removeItem('loadAssistantConfig')
96
+ } catch (error) {
97
+ console.error('Failed to load assistant config:', error)
98
+ }
99
+ }
100
+ }
101
+
102
+ const loadAssistant = (assistant: AssistantConfig) => {
103
+ setSelectedAssistant(assistant)
104
+ applyAssistantConfig(assistant)
105
+
106
+ // Create a new session for this assistant
107
+ createNewSession()
108
+ }
109
+
110
+ const applyAssistantConfig = (config: AssistantConfig) => {
111
+ setSelectedModel(config.model)
112
+ setSystemPrompt(config.instructions)
113
+ setTemperature(config.temperature)
114
+ setMaxTokens(config.maxTokens)
115
+ }
116
+
117
+ const editCurrentAssistant = () => {
118
+ if (!selectedAssistant) return
119
+
120
+ localStorage.setItem('editAssistantConfig', JSON.stringify(selectedAssistant))
121
+ window.location.href = '/assistant-builder'
122
+ }
123
+
124
+ return (
125
+ <div className="flex h-screen bg-background">
126
+
127
+ {/* Sidebar - Assistant Selection */}
128
+ <div className={`${showSessions ? 'w-80' : 'w-0'} transition-all duration-200 overflow-hidden border-r bg-background/95`}>
129
+ <div className="p-4 border-b">
130
+ <div className="flex items-center justify-between mb-4">
131
+ <h2 className="text-lg font-semibold">My Assistants</h2>
132
+ <Button
133
+ size="sm"
134
+ onClick={() => window.location.href = '/assistant-builder'}
135
+ >
136
+ <Plus className="h-4 w-4" />
137
+ </Button>
138
+ </div>
139
+
140
+ {/* Assistant List */}
141
+ <div className="space-y-2">
142
+ {savedAssistants.map((assistant) => (
143
+ <Card
144
+ key={assistant.id}
145
+ className={`cursor-pointer hover:bg-accent transition-colors ${
146
+ selectedAssistant?.id === assistant.id ? 'ring-2 ring-blue-500' : ''
147
+ }`}
148
+ onClick={() => loadAssistant(assistant)}
149
+ >
150
+ <CardContent className="p-3">
151
+ <div className="flex items-start gap-2">
152
+ <Bot className="h-4 w-4 mt-1 text-purple-600" />
153
+ <div className="flex-1 min-w-0">
154
+ <h3 className="font-medium text-sm truncate">{assistant.name}</h3>
155
+ <p className="text-xs text-muted-foreground truncate">
156
+ {assistant.description || 'No description'}
157
+ </p>
158
+ <Badge variant="secondary" className="text-xs mt-1">
159
+ {assistant.model}
160
+ </Badge>
161
+ </div>
162
+ </div>
163
+ </CardContent>
164
+ </Card>
165
+ ))}
166
+
167
+ {savedAssistants.length === 0 && (
168
+ <div className="text-center py-8">
169
+ <Bot className="h-8 w-8 mx-auto text-muted-foreground mb-2" />
170
+ <p className="text-sm text-muted-foreground">No assistants yet</p>
171
+ <Button
172
+ size="sm"
173
+ variant="outline"
174
+ className="mt-2"
175
+ onClick={() => window.location.href = '/assistant-builder'}
176
+ >
177
+ Create One
178
+ </Button>
179
+ </div>
180
+ )}
181
+ </div>
182
+ </div>
183
+
184
+ {/* Chat Sessions */}
185
+ <div className="p-4">
186
+ <div className="flex items-center justify-between mb-3">
187
+ <h3 className="text-sm font-medium">Sessions</h3>
188
+ <Button size="sm" variant="ghost" onClick={createNewSession}>
189
+ <Plus className="h-3 w-3" />
190
+ </Button>
191
+ </div>
192
+
193
+ <div className="space-y-1">
194
+ {sessions.map((session) => (
195
+ <div
196
+ key={session.id}
197
+ className={`flex items-center justify-between p-2 rounded-md cursor-pointer hover:bg-accent ${
198
+ currentSessionId === session.id ? 'bg-accent' : ''
199
+ }`}
200
+ >
201
+ <div className="flex-1 min-w-0" onClick={() => selectSession(session.id)}>
202
+ <p className="text-xs truncate">{session.title}</p>
203
+ <p className="text-xs text-muted-foreground">
204
+ {session.messages.length} messages
205
+ </p>
206
+ </div>
207
+ <Button
208
+ size="sm"
209
+ variant="ghost"
210
+ onClick={() => deleteSession(session.id)}
211
+ className="h-6 w-6 p-0"
212
+ >
213
+ <Trash2 className="h-3 w-3" />
214
+ </Button>
215
+ </div>
216
+ ))}
217
+ </div>
218
+ </div>
219
+ </div>
220
+
221
+ {/* Main Chat Area */}
222
+ <div className="flex-1 flex flex-col">
223
+
224
+ {/* Header */}
225
+ <div className="border-b p-4">
226
+ <div className="flex items-center justify-between">
227
+ <div className="flex items-center gap-4">
228
+ <Button
229
+ variant="ghost"
230
+ size="sm"
231
+ onClick={() => setShowSessions(!showSessions)}
232
+ >
233
+ {showSessions ? (
234
+ <PanelLeftClose className="h-4 w-4" />
235
+ ) : (
236
+ <PanelLeftOpen className="h-4 w-4" />
237
+ )}
238
+ </Button>
239
+
240
+ {selectedAssistant ? (
241
+ <div className="flex items-center gap-3">
242
+ <div className="w-8 h-8 bg-purple-600 rounded-lg flex items-center justify-center">
243
+ <Bot className="h-4 w-4 text-white" />
244
+ </div>
245
+ <div>
246
+ <h1 className="font-semibold">{selectedAssistant.name}</h1>
247
+ <p className="text-sm text-muted-foreground">
248
+ {selectedAssistant.description}
249
+ </p>
250
+ </div>
251
+ </div>
252
+ ) : (
253
+ <div>
254
+ <h1 className="font-semibold">Select an Assistant</h1>
255
+ <p className="text-sm text-muted-foreground">
256
+ Choose an assistant to start chatting
257
+ </p>
258
+ </div>
259
+ )}
260
+ </div>
261
+
262
+ <div className="flex items-center gap-2">
263
+ {!selectedAssistant && savedAssistants.length === 0 && (
264
+ <Button onClick={() => window.location.href = '/assistant-builder'}>
265
+ <Plus className="h-4 w-4 mr-2" />
266
+ Create Assistant
267
+ </Button>
268
+ )}
269
+
270
+ {selectedAssistant && (
271
+ <>
272
+ <Button variant="outline" size="sm" onClick={editCurrentAssistant}>
273
+ <Settings className="h-4 w-4" />
274
+ </Button>
275
+
276
+ <div className="flex items-center gap-2">
277
+ <Badge variant="secondary" className="text-xs">
278
+ {selectedAssistant.model}
279
+ </Badge>
280
+ <Badge variant="outline" className="text-xs">
281
+ T: {selectedAssistant.temperature}
282
+ </Badge>
283
+ <Badge variant="outline" className="text-xs">
284
+ Max: {selectedAssistant.maxTokens}
285
+ </Badge>
286
+ </div>
287
+ </>
288
+ )}
289
+ </div>
290
+ </div>
291
+ </div>
292
+
293
+ {/* Chat Interface */}
294
+ <div className="flex-1 overflow-hidden">
295
+ {selectedAssistant ? (
296
+ <Chat
297
+ messages={messages}
298
+ input={input}
299
+ handleInputChange={(e) => setInput(e.target.value)}
300
+ handleSubmit={(e) => {
301
+ e.preventDefault()
302
+ sendMessage()
303
+ }}
304
+ isGenerating={isLoading}
305
+ stop={stopGeneration}
306
+ />
307
+ ) : (
308
+ <div className="flex items-center justify-center h-full">
309
+ <div className="text-center max-w-md">
310
+ <MessageSquare className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
311
+ <h2 className="text-xl font-semibold mb-2">Welcome to Chat Interface</h2>
312
+ <p className="text-muted-foreground mb-6">
313
+ Select an assistant from the sidebar to start a conversation, or create a new one.
314
+ </p>
315
+ <div className="flex items-center justify-center gap-3">
316
+ {savedAssistants.length > 0 ? (
317
+ <Button onClick={() => setShowSessions(true)}>
318
+ <History className="h-4 w-4 mr-2" />
319
+ Choose Assistant
320
+ </Button>
321
+ ) : null}
322
+ <Button onClick={() => window.location.href = '/assistant-builder'}>
323
+ <Plus className="h-4 w-4 mr-2" />
324
+ Create Assistant
325
+ </Button>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ )}
330
+ </div>
331
+ </div>
332
+ </div>
333
+ )
334
+ }
frontend/src/pages/Home.tsx CHANGED
@@ -11,7 +11,8 @@ import {
11
  Zap,
12
  Shield,
13
  Cpu,
14
- Brain
 
15
  } from 'lucide-react'
16
 
17
  export function Home() {
@@ -53,42 +54,56 @@ export function Home() {
53
  }
54
  }
55
 
56
- const quickActions = [
57
  {
58
- title: 'Quick Chat',
59
- description: 'Start chatting immediately - no setup required',
 
60
  icon: MessageSquare,
61
- href: '/chat',
62
  color: 'bg-blue-600',
63
  primary: true,
64
- badge: 'Instant'
 
65
  },
66
  {
67
- title: 'Build Assistant',
68
- description: 'Create your custom AI assistant with specialized skills',
 
69
  icon: Bot,
70
  href: '/assistant-builder',
71
  color: 'bg-purple-600',
72
  primary: true,
73
- badge: 'Advanced'
 
74
  },
75
  {
76
- title: 'Use Template',
77
- description: 'Start with pre-built assistant templates from the community',
78
- icon: Users,
79
- href: '/community',
 
80
  color: 'bg-green-600',
81
  primary: true,
82
- badge: 'Popular'
83
- },
 
 
 
 
84
  {
85
  title: 'Manage Models',
86
- description: 'Configure and load AI models',
87
  icon: BookOpen,
88
  href: '/models',
89
- color: 'bg-gray-600',
90
- primary: false,
91
- badge: null
 
 
 
 
 
92
  }
93
  ]
94
 
@@ -138,87 +153,100 @@ export function Home() {
138
  Complete privacy, advanced features, industrial-grade performance.
139
  </p>
140
 
141
- <div className="flex items-center justify-center gap-4 pt-4">
142
- <Button size="lg" onClick={() => window.location.href = '/playground'} className="flex items-center gap-2">
143
- <MessageSquare className="h-5 w-5" />
144
- Start Chatting
145
- <ArrowRight className="h-4 w-4" />
146
- </Button>
147
-
148
- <Button size="lg" variant="outline" onClick={() => window.location.href = '/models'}>
149
- <BookOpen className="h-5 w-5 mr-2" />
150
- Browse Models
151
- </Button>
152
- </div>
153
  </div>
154
  </div>
155
  </div>
156
 
157
  <div className="max-w-6xl mx-auto px-6 py-12 space-y-12">
158
 
159
- {/* Quick Actions */}
160
- <section>
161
- <div className="text-center mb-8">
162
- <h2 className="text-2xl font-semibold mb-3">Choose Your Path</h2>
163
- <p className="text-gray-600 max-w-2xl mx-auto">
164
- Select the best way to get started based on your needs and experience level
165
- </p>
166
- </div>
167
-
168
- {/* Primary Actions */}
169
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
170
- {quickActions.filter(action => action.primary).map((action) => (
171
- <Card
172
- key={action.title}
173
- className="hover:shadow-xl transition-all duration-300 cursor-pointer hover:-translate-y-2 border-2 border-transparent hover:border-blue-200"
174
- onClick={() => window.location.href = action.href}
175
- >
176
- <CardContent className="p-8 text-center space-y-6">
177
- <div className="relative">
178
- <div className={`w-16 h-16 ${action.color} rounded-2xl flex items-center justify-center mx-auto`}>
179
- <action.icon className="h-8 w-8 text-white" />
180
- </div>
181
- {action.badge && (
182
- <Badge className="absolute -top-2 -right-2 bg-white text-gray-700 border border-gray-200">
183
- {action.badge}
184
- </Badge>
185
- )}
186
- </div>
187
- <div>
188
- <h3 className="font-bold text-xl mb-2">{action.title}</h3>
189
- <p className="text-gray-600 text-sm leading-relaxed">{action.description}</p>
190
- </div>
191
- <Button className="w-full" size="lg">
192
- Get Started
193
- <ArrowRight className="h-4 w-4 ml-2" />
194
- </Button>
195
- </CardContent>
196
- </Card>
197
- ))}
198
- </div>
199
-
200
- {/* Secondary Actions */}
201
- <div className="grid grid-cols-1 md:grid-cols-1 gap-4">
202
- {quickActions.filter(action => !action.primary).map((action) => (
203
- <Card
204
- key={action.title}
205
- className="hover:shadow-md transition-all duration-200 cursor-pointer hover:-translate-y-1"
206
- onClick={() => window.location.href = action.href}
207
- >
208
- <CardContent className="p-4 flex items-center gap-4">
209
- <div className={`w-12 h-12 ${action.color} rounded-lg flex items-center justify-center`}>
210
- <action.icon className="h-6 w-6 text-white" />
211
- </div>
212
- <div className="flex-1">
213
- <h3 className="font-semibold text-base">{action.title}</h3>
214
- <p className="text-sm text-gray-600">{action.description}</p>
215
- </div>
216
- <ArrowRight className="h-5 w-5 text-gray-400" />
217
- </CardContent>
218
- </Card>
219
- ))}
220
- </div>
221
- </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  {/* System Status */}
224
  <section className="grid grid-cols-1 md:grid-cols-3 gap-6">
@@ -264,29 +292,29 @@ export function Home() {
264
  </div>
265
  </section>
266
 
267
- {/* Recommended Path */}
268
- <section className="bg-gradient-to-r from-blue-50 to-purple-50 rounded-2xl p-8 border border-blue-200">
269
- <div className="text-center">
270
- <div className="w-12 h-12 bg-blue-600 rounded-xl flex items-center justify-center mx-auto mb-4">
271
- <Zap className="h-6 w-6 text-white" />
272
- </div>
273
- <h2 className="text-2xl font-semibold mb-4">New to AI Assistants?</h2>
274
- <p className="text-gray-600 mb-6 max-w-2xl mx-auto">
275
- We recommend starting with <strong>Quick Chat</strong> to get familiar with AI conversations,
276
- then try our <strong>Community Templates</strong> to see what's possible.
277
- </p>
278
- <div className="flex items-center justify-center gap-4">
279
- <Button size="lg" onClick={() => window.location.href = '/chat'}>
280
- <MessageSquare className="h-4 w-4 mr-2" />
281
- Try Quick Chat
282
- </Button>
283
- <Button size="lg" variant="outline" onClick={() => window.location.href = '/community'}>
284
- <Users className="h-4 w-4 mr-2" />
285
- Browse Templates
286
- </Button>
287
- </div>
288
- </div>
289
- </section>
290
  </div>
291
  </div>
292
  )
 
11
  Zap,
12
  Shield,
13
  Cpu,
14
+ Brain,
15
+ Sparkles
16
  } from 'lucide-react'
17
 
18
  export function Home() {
 
54
  }
55
  }
56
 
57
+ const workflows = [
58
  {
59
+ title: 'Quick Start',
60
+ subtitle: 'New to AI assistants?',
61
+ description: 'Start with pre-built templates from our community',
62
  icon: MessageSquare,
63
+ href: '/community',
64
  color: 'bg-blue-600',
65
  primary: true,
66
+ badge: 'Recommended',
67
+ steps: ['Browse templates', 'Select one that fits', 'Start chatting immediately']
68
  },
69
  {
70
+ title: 'Custom Assistant',
71
+ subtitle: 'Need something specific?',
72
+ description: 'Create your own AI assistant with custom instructions',
73
  icon: Bot,
74
  href: '/assistant-builder',
75
  color: 'bg-purple-600',
76
  primary: true,
77
+ badge: 'Advanced',
78
+ steps: ['Define your assistant', 'Set parameters', 'Test and refine']
79
  },
80
  {
81
+ title: 'Quick Chat',
82
+ subtitle: 'Already have assistants?',
83
+ description: 'Jump directly into conversations with your saved assistants',
84
+ icon: Zap,
85
+ href: '/chat',
86
  color: 'bg-green-600',
87
  primary: true,
88
+ badge: 'Fast',
89
+ steps: ['Select assistant', 'Start conversation', 'Get instant results']
90
+ }
91
+ ]
92
+
93
+ const supportActions = [
94
  {
95
  title: 'Manage Models',
96
+ description: 'Configure AI models and settings',
97
  icon: BookOpen,
98
  href: '/models',
99
+ color: 'bg-gray-600'
100
+ },
101
+ {
102
+ title: 'My Assistants',
103
+ description: 'View and organize your saved assistants',
104
+ icon: Users,
105
+ href: '/assistants',
106
+ color: 'bg-orange-600'
107
  }
108
  ]
109
 
 
153
  Complete privacy, advanced features, industrial-grade performance.
154
  </p>
155
 
156
+ <div className="flex items-center justify-center gap-4 pt-4">
157
+ <Button size="lg" onClick={() => window.location.href = '/community'} className="flex items-center gap-2">
158
+ <MessageSquare className="h-5 w-5" />
159
+ Get Started
160
+ <ArrowRight className="h-4 w-4" />
161
+ </Button>
162
+
163
+ <Button size="lg" variant="outline" onClick={() => window.location.href = '/assistant-builder'}>
164
+ <Bot className="h-5 w-5 mr-2" />
165
+ Create Assistant
166
+ </Button>
167
+ </div>
168
  </div>
169
  </div>
170
  </div>
171
 
172
  <div className="max-w-6xl mx-auto px-6 py-12 space-y-12">
173
 
174
+ {/* Workflow Selection */}
175
+ <section>
176
+ <div className="text-center mb-8">
177
+ <h2 className="text-2xl font-semibold mb-3">Choose Your Path</h2>
178
+ <p className="text-gray-600 max-w-2xl mx-auto">
179
+ Select the workflow that best matches your experience level and needs
180
+ </p>
181
+ </div>
182
+
183
+ {/* Primary Workflows */}
184
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
185
+ {workflows.map((workflow) => (
186
+ <Card
187
+ key={workflow.title}
188
+ className="hover:shadow-xl transition-all duration-300 cursor-pointer hover:-translate-y-2 border-2 border-transparent hover:border-blue-200 relative overflow-hidden"
189
+ onClick={() => window.location.href = workflow.href}
190
+ >
191
+ <div className="absolute top-4 right-4">
192
+ <Badge className="bg-white text-gray-700 border border-gray-200 shadow-sm">
193
+ {workflow.badge}
194
+ </Badge>
195
+ </div>
196
+
197
+ <CardContent className="p-8 text-center space-y-6">
198
+ <div className={`w-16 h-16 ${workflow.color} rounded-2xl flex items-center justify-center mx-auto`}>
199
+ <workflow.icon className="h-8 w-8 text-white" />
200
+ </div>
201
+
202
+ <div>
203
+ <h3 className="font-bold text-xl mb-1">{workflow.title}</h3>
204
+ <p className="text-sm text-blue-600 font-medium mb-3">{workflow.subtitle}</p>
205
+ <p className="text-gray-600 text-sm leading-relaxed mb-4">{workflow.description}</p>
206
+ </div>
207
+
208
+ <div className="space-y-2 text-left">
209
+ {workflow.steps.map((step, index) => (
210
+ <div key={index} className="flex items-center gap-2 text-sm text-gray-600">
211
+ <div className="w-5 h-5 bg-gray-100 rounded-full flex items-center justify-center text-xs font-medium">
212
+ {index + 1}
213
+ </div>
214
+ <span>{step}</span>
215
+ </div>
216
+ ))}
217
+ </div>
218
+
219
+ <Button className="w-full" size="lg">
220
+ Get Started
221
+ <ArrowRight className="h-4 w-4 ml-2" />
222
+ </Button>
223
+ </CardContent>
224
+ </Card>
225
+ ))}
226
+ </div>
227
+
228
+ {/* Support Actions */}
229
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
230
+ {supportActions.map((action) => (
231
+ <Card
232
+ key={action.title}
233
+ className="hover:shadow-md transition-all duration-200 cursor-pointer hover:-translate-y-1"
234
+ onClick={() => window.location.href = action.href}
235
+ >
236
+ <CardContent className="p-4 flex items-center gap-4">
237
+ <div className={`w-12 h-12 ${action.color} rounded-lg flex items-center justify-center`}>
238
+ <action.icon className="h-6 w-6 text-white" />
239
+ </div>
240
+ <div className="flex-1">
241
+ <h3 className="font-semibold text-base">{action.title}</h3>
242
+ <p className="text-sm text-gray-600">{action.description}</p>
243
+ </div>
244
+ <ArrowRight className="h-5 w-5 text-gray-400" />
245
+ </CardContent>
246
+ </Card>
247
+ ))}
248
+ </div>
249
+ </section>
250
 
251
  {/* System Status */}
252
  <section className="grid grid-cols-1 md:grid-cols-3 gap-6">
 
292
  </div>
293
  </section>
294
 
295
+ {/* Recommendation */}
296
+ <section className="bg-gradient-to-r from-blue-50 to-purple-50 rounded-2xl p-8 border border-blue-200">
297
+ <div className="text-center">
298
+ <div className="w-12 h-12 bg-blue-600 rounded-xl flex items-center justify-center mx-auto mb-4">
299
+ <Sparkles className="h-6 w-6 text-white" />
300
+ </div>
301
+ <h2 className="text-2xl font-semibold mb-4">New to AI Assistants?</h2>
302
+ <p className="text-gray-600 mb-6 max-w-2xl mx-auto">
303
+ We recommend starting with our <strong>Community Templates</strong> to see what's possible,
304
+ then try creating your own custom assistant when you're ready.
305
+ </p>
306
+ <div className="flex items-center justify-center gap-4">
307
+ <Button size="lg" onClick={() => window.location.href = '/community'}>
308
+ <Users className="h-4 w-4 mr-2" />
309
+ Browse Templates
310
+ </Button>
311
+ <Button size="lg" variant="outline" onClick={() => window.location.href = '/assistant-builder'}>
312
+ <Bot className="h-4 w-4 mr-2" />
313
+ Create Assistant
314
+ </Button>
315
+ </div>
316
+ </div>
317
+ </section>
318
  </div>
319
  </div>
320
  )
frontend/src/pages/PlaygroundRedirect.tsx DELETED
@@ -1,100 +0,0 @@
1
- import { useEffect } from 'react'
2
- import { Card, CardContent } from '@/components/ui/card'
3
- import { Button } from '@/components/ui/button'
4
- import {
5
- MessageSquare,
6
- Bot,
7
- ArrowRight,
8
- Zap
9
- } from 'lucide-react'
10
-
11
- export function Playground() {
12
- useEffect(() => {
13
- // Auto-redirect to Quick Chat after 3 seconds if user doesn't choose
14
- const timer = setTimeout(() => {
15
- window.location.href = '/chat'
16
- }, 5000)
17
-
18
- return () => clearTimeout(timer)
19
- }, [])
20
-
21
- const options = [
22
- {
23
- title: 'Quick Chat',
24
- description: 'Simple AI conversations with no configuration needed',
25
- href: '/chat',
26
- icon: MessageSquare,
27
- color: 'bg-blue-600',
28
- recommended: true
29
- },
30
- {
31
- title: 'Build Assistant',
32
- description: 'Create a custom AI assistant with specialized skills',
33
- href: '/assistant-builder',
34
- icon: Bot,
35
- color: 'bg-purple-600',
36
- recommended: false
37
- }
38
- ]
39
-
40
- return (
41
- <div className="min-h-screen bg-background flex items-center justify-center p-6">
42
- <div className="max-w-4xl w-full">
43
- <div className="text-center mb-12">
44
- <div className="w-16 h-16 bg-blue-600 rounded-2xl flex items-center justify-center mx-auto mb-6">
45
- <Zap className="h-8 w-8 text-white" />
46
- </div>
47
- <h1 className="text-3xl font-bold mb-4">Choose Your Experience</h1>
48
- <p className="text-xl text-gray-600 max-w-2xl mx-auto">
49
- The Playground has been redesigned! Choose the experience that best fits your needs.
50
- </p>
51
- </div>
52
-
53
- <div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
54
- {options.map((option) => (
55
- <Card
56
- key={option.title}
57
- className={`cursor-pointer hover:shadow-xl transition-all duration-300 hover:-translate-y-2 ${
58
- option.recommended ? 'ring-2 ring-blue-200 bg-blue-50' : ''
59
- }`}
60
- onClick={() => window.location.href = option.href}
61
- >
62
- <CardContent className="p-8 text-center space-y-6">
63
- {option.recommended && (
64
- <div className="bg-blue-600 text-white text-xs px-3 py-1 rounded-full inline-block mb-4">
65
- Recommended for most users
66
- </div>
67
- )}
68
- <div className={`w-16 h-16 ${option.color} rounded-2xl flex items-center justify-center mx-auto`}>
69
- <option.icon className="h-8 w-8 text-white" />
70
- </div>
71
- <div>
72
- <h3 className="font-bold text-2xl mb-3">{option.title}</h3>
73
- <p className="text-gray-600 leading-relaxed">{option.description}</p>
74
- </div>
75
- <Button size="lg" className="w-full">
76
- Continue
77
- <ArrowRight className="h-4 w-4 ml-2" />
78
- </Button>
79
- </CardContent>
80
- </Card>
81
- ))}
82
- </div>
83
-
84
- <div className="text-center">
85
- <p className="text-sm text-gray-500 mb-4">
86
- This page will automatically redirect to Quick Chat in a few seconds
87
- </p>
88
- <div className="flex items-center justify-center gap-4">
89
- <Button variant="outline" onClick={() => window.location.href = '/'}>
90
- Back to Home
91
- </Button>
92
- <Button onClick={() => window.location.href = '/chat'}>
93
- Go to Quick Chat
94
- </Button>
95
- </div>
96
- </div>
97
- </div>
98
- </div>
99
- )
100
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/SimpleChat.tsx DELETED
@@ -1,298 +0,0 @@
1
- import { useState, useEffect, useRef } from 'react'
2
- import { Card, CardContent } from '@/components/ui/card'
3
- import { Button } from '@/components/ui/button'
4
- import { Badge } from '@/components/ui/badge'
5
- import { Textarea } from '@/components/ui/textarea'
6
- import {
7
- Send,
8
- Bot,
9
- User,
10
- MessageSquare,
11
- Plus,
12
- Sparkles
13
- } from 'lucide-react'
14
-
15
- interface Message {
16
- id: string
17
- role: 'user' | 'assistant' | 'system'
18
- content: string
19
- timestamp: Date
20
- }
21
-
22
- export function SimpleChat() {
23
- const [messages, setMessages] = useState<Message[]>([])
24
- const [input, setInput] = useState('')
25
- const [isLoading, setIsLoading] = useState(false)
26
- const [selectedModel] = useState('Qwen/Qwen3-30B-A3B')
27
- const textareaRef = useRef<HTMLTextAreaElement>(null)
28
-
29
- useEffect(() => {
30
- loadChatHistory()
31
- }, [])
32
-
33
- const loadChatHistory = () => {
34
- try {
35
- const saved = localStorage.getItem('simpleChatHistory')
36
- if (saved) {
37
- const history = JSON.parse(saved)
38
- setMessages(history.map((msg: any) => ({
39
- ...msg,
40
- timestamp: new Date(msg.timestamp)
41
- })))
42
- }
43
- } catch (error) {
44
- console.error('Failed to load chat history:', error)
45
- }
46
- }
47
-
48
- const saveChatHistory = (msgs: Message[]) => {
49
- try {
50
- localStorage.setItem('simpleChatHistory', JSON.stringify(msgs))
51
- } catch (error) {
52
- console.error('Failed to save chat history:', error)
53
- }
54
- }
55
-
56
- const sendMessage = async () => {
57
- if (!input.trim()) return
58
-
59
- const userMessage: Message = {
60
- id: `user_${Date.now()}`,
61
- role: 'user',
62
- content: input.trim(),
63
- timestamp: new Date()
64
- }
65
-
66
- const newMessages = [...messages, userMessage]
67
- setMessages(newMessages)
68
- setInput('')
69
- setIsLoading(true)
70
-
71
- try {
72
- const baseUrl = `${window.location.protocol}//${window.location.host}`
73
- const response = await fetch(`${baseUrl}/chat/completions`, {
74
- method: 'POST',
75
- headers: { 'Content-Type': 'application/json' },
76
- body: JSON.stringify({
77
- messages: newMessages.map(msg => ({
78
- role: msg.role,
79
- content: msg.content
80
- })),
81
- temperature: 0.7,
82
- max_tokens: 2000
83
- })
84
- })
85
-
86
- if (response.ok) {
87
- const data = await response.json()
88
- const assistantMessage: Message = {
89
- id: `assistant_${Date.now()}`,
90
- role: 'assistant',
91
- content: data.choices[0]?.message?.content || 'No response received',
92
- timestamp: new Date()
93
- }
94
-
95
- const finalMessages = [...newMessages, assistantMessage]
96
- setMessages(finalMessages)
97
- saveChatHistory(finalMessages)
98
- } else {
99
- throw new Error(`HTTP ${response.status}`)
100
- }
101
- } catch (error) {
102
- console.error('Chat error:', error)
103
- const errorMessage: Message = {
104
- id: `error_${Date.now()}`,
105
- role: 'assistant',
106
- content: 'Sorry, I encountered an error. Please try again.',
107
- timestamp: new Date()
108
- }
109
- const finalMessages = [...newMessages, errorMessage]
110
- setMessages(finalMessages)
111
- saveChatHistory(finalMessages)
112
- } finally {
113
- setIsLoading(false)
114
- }
115
- }
116
-
117
- const handleKeyPress = (e: React.KeyboardEvent) => {
118
- if (e.key === 'Enter' && !e.shiftKey) {
119
- e.preventDefault()
120
- sendMessage()
121
- }
122
- }
123
-
124
- const clearChat = () => {
125
- setMessages([])
126
- localStorage.removeItem('simpleChatHistory')
127
- }
128
-
129
- const createAssistantFromChat = () => {
130
- // Save current conversation as starting point for assistant
131
- const conversation = messages.slice(-10) // Last 10 messages
132
- localStorage.setItem('assistantBuilderContext', JSON.stringify({
133
- messages: conversation,
134
- model: selectedModel
135
- }))
136
- window.location.href = '/assistant-builder'
137
- }
138
-
139
- const quickPrompts = [
140
- "Help me write a professional email",
141
- "Explain quantum computing simply",
142
- "Create a workout plan",
143
- "Review my code for bugs"
144
- ]
145
-
146
- return (
147
- <div className="min-h-screen bg-background">
148
- {/* Header */}
149
- <div className="border-b bg-background/95 backdrop-blur">
150
- <div className="max-w-4xl mx-auto px-6 py-4">
151
- <div className="flex items-center justify-between">
152
- <div className="flex items-center gap-3">
153
- <div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
154
- <MessageSquare className="h-5 w-5 text-white" />
155
- </div>
156
- <div>
157
- <h1 className="text-xl font-semibold">Quick Chat</h1>
158
- <p className="text-sm text-muted-foreground">
159
- Simple AI conversations - no setup required
160
- </p>
161
- </div>
162
- </div>
163
- <div className="flex items-center gap-2">
164
- <Badge variant="outline" className="text-xs">
165
- {selectedModel.split('/').pop()}
166
- </Badge>
167
- <Button variant="outline" size="sm" onClick={clearChat}>
168
- <Plus className="h-4 w-4 mr-1" />
169
- New Chat
170
- </Button>
171
- {messages.length > 0 && (
172
- <Button size="sm" onClick={createAssistantFromChat}>
173
- <Sparkles className="h-4 w-4 mr-1" />
174
- Create Assistant
175
- </Button>
176
- )}
177
- </div>
178
- </div>
179
- </div>
180
- </div>
181
-
182
- <div className="max-w-4xl mx-auto px-6 py-6">
183
- <div className="flex flex-col h-[calc(100vh-200px)]">
184
-
185
- {/* Messages Area */}
186
- <div className="flex-1 mb-6 overflow-hidden">
187
- <Card className="h-full">
188
- <CardContent className="h-full p-0">
189
- <div className="h-full p-6 overflow-y-auto">
190
- {messages.length === 0 ? (
191
- <div className="flex flex-col items-center justify-center h-full text-center">
192
- <div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-4">
193
- <Bot className="h-8 w-8 text-blue-600" />
194
- </div>
195
- <h3 className="text-lg font-medium mb-2">Start a conversation</h3>
196
- <p className="text-muted-foreground mb-6 max-w-md">
197
- Ask me anything or try one of these suggestions to get started
198
- </p>
199
- <div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-w-2xl">
200
- {quickPrompts.map((prompt, index) => (
201
- <Button
202
- key={index}
203
- variant="outline"
204
- className="text-left h-auto py-3 px-4"
205
- onClick={() => setInput(prompt)}
206
- >
207
- <div className="text-sm">{prompt}</div>
208
- </Button>
209
- ))}
210
- </div>
211
- </div>
212
- ) : (
213
- <div className="space-y-6">
214
- {messages.map((message) => (
215
- <div key={message.id} className={`flex gap-4 ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
216
- {message.role === 'assistant' && (
217
- <div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
218
- <Bot className="h-4 w-4 text-white" />
219
- </div>
220
- )}
221
- <div className={`max-w-[80%] p-4 rounded-lg ${
222
- message.role === 'user'
223
- ? 'bg-blue-600 text-white'
224
- : 'bg-gray-100 text-gray-900'
225
- }`}>
226
- <div className="prose prose-sm max-w-none">
227
- <p className="whitespace-pre-wrap">{message.content}</p>
228
- </div>
229
- </div>
230
- {message.role === 'user' && (
231
- <div className="w-8 h-8 bg-gray-600 rounded-full flex items-center justify-center">
232
- <User className="h-4 w-4 text-white" />
233
- </div>
234
- )}
235
- </div>
236
- ))}
237
- {isLoading && (
238
- <div className="flex gap-4 justify-start">
239
- <div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
240
- <Bot className="h-4 w-4 text-white" />
241
- </div>
242
- <div className="bg-gray-100 rounded-lg p-4">
243
- <div className="flex space-x-1">
244
- <div className="w-2 h-2 bg-gray-400 rounded-full animate-pulse"></div>
245
- <div className="w-2 h-2 bg-gray-400 rounded-full animate-pulse" style={{ animationDelay: '0.2s' }}></div>
246
- <div className="w-2 h-2 bg-gray-400 rounded-full animate-pulse" style={{ animationDelay: '0.4s' }}></div>
247
- </div>
248
- </div>
249
- </div>
250
- )}
251
- </div>
252
- )}
253
- </div>
254
- </CardContent>
255
- </Card>
256
- </div>
257
-
258
- {/* Input Area */}
259
- <div className="space-y-4">
260
- <Card>
261
- <CardContent className="p-4">
262
- <div className="flex gap-3">
263
- <Textarea
264
- ref={textareaRef}
265
- placeholder="Type your message..."
266
- value={input}
267
- onChange={(e) => setInput(e.target.value)}
268
- onKeyPress={handleKeyPress}
269
- className="flex-1 min-h-[60px] resize-none"
270
- disabled={isLoading}
271
- />
272
- <Button
273
- onClick={sendMessage}
274
- disabled={isLoading || !input.trim()}
275
- size="lg"
276
- className="shrink-0"
277
- >
278
- {isLoading ? (
279
- <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
280
- ) : (
281
- <Send className="h-4 w-4" />
282
- )}
283
- </Button>
284
- </div>
285
- </CardContent>
286
- </Card>
287
-
288
- <div className="text-center">
289
- <p className="text-xs text-muted-foreground">
290
- Press Enter to send, Shift+Enter for new line
291
- </p>
292
- </div>
293
- </div>
294
- </div>
295
- </div>
296
- </div>
297
- )
298
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/assets/index-6f63b786.js ADDED
The diff for this file is too large to render. See raw diff
 
static/assets/index-6f63b786.js.map ADDED
The diff for this file is too large to render. See raw diff
 
static/assets/index-f766ffe0.css ADDED
@@ -0,0 +1 @@
 
 
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 222.2 84% 4.9%;--card: 0 0% 100%;--card-foreground: 222.2 84% 4.9%;--popover: 0 0% 100%;--popover-foreground: 222.2 84% 4.9%;--primary: 221.2 83.2% 53.3%;--primary-foreground: 210 40% 98%;--secondary: 210 40% 96%;--secondary-foreground: 222.2 84% 4.9%;--muted: 210 40% 96%;--muted-foreground: 215.4 16.3% 46.9%;--accent: 210 40% 96%;--accent-foreground: 222.2 84% 4.9%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 210 40% 98%;--border: 214.3 31.8% 91.4%;--input: 214.3 31.8% 91.4%;--ring: 221.2 83.2% 53.3%;--radius: .5rem}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-y-0{top:0;bottom:0}.left-0{left:0}.left-2{left:.5rem}.left-\[50\%\]{left:50%}.right-4{right:1rem}.top-4{top:1rem}.top-\[50\%\]{top:50%}.z-40{z-index:40}.z-50{z-index:50}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-96{max-height:24rem}.min-h-\[300px\]{min-height:300px}.min-h-\[60px\]{min-height:60px}.min-h-\[80px\]{min-height:80px}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-16{width:4rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[8rem\]{min-width:8rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-6xl{max-width:72rem}.max-w-\[75\%\]{max-width:75%}.max-w-\[80\%\]{max-width:80%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-x-full{--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-\[-50\%\]{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-\[-50\%\]{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-12>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(3rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(3rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity, 1))}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity, 1))}.border-input{border-color:hsl(var(--input))}.border-muted{border-color:hsl(var(--muted))}.border-primary{border-color:hsl(var(--primary))}.border-purple-200{--tw-border-opacity: 1;border-color:rgb(233 213 255 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity, 1))}.bg-accent{background-color:hsl(var(--accent))}.bg-background{background-color:hsl(var(--background))}.bg-background\/50{background-color:hsl(var(--background) / .5)}.bg-background\/80{background-color:hsl(var(--background) / .8)}.bg-background\/95{background-color:hsl(var(--background) / .95)}.bg-black\/50{background-color:#00000080}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-gray-600{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-muted{background-color:hsl(var(--muted))}.bg-muted\/30{background-color:hsl(var(--muted) / .3)}.bg-orange-600{--tw-bg-opacity: 1;background-color:rgb(234 88 12 / var(--tw-bg-opacity, 1))}.bg-popover{background-color:hsl(var(--popover))}.bg-primary{background-color:hsl(var(--primary))}.bg-purple-50{--tw-bg-opacity: 1;background-color:rgb(250 245 255 / var(--tw-bg-opacity, 1))}.bg-purple-600{--tw-bg-opacity: 1;background-color:rgb(147 51 234 / var(--tw-bg-opacity, 1))}.bg-secondary{background-color:hsl(var(--secondary))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity, 1))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-blue-50{--tw-gradient-from: #eff6ff var(--tw-gradient-from-position);--tw-gradient-to: rgb(239 246 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-indigo-100{--tw-gradient-to: #e0e7ff var(--tw-gradient-to-position)}.to-purple-50{--tw-gradient-to: #faf5ff var(--tw-gradient-to-position)}.fill-current{fill:currentColor}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-3{padding-bottom:.75rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pr-2{padding-right:.5rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.text-accent-foreground{color:hsl(var(--accent-foreground))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-blue-900{--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-foreground{color:hsl(var(--foreground))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-orange-600{--tw-text-opacity: 1;color:rgb(234 88 12 / var(--tw-text-opacity, 1))}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-purple-500{--tw-text-opacity: 1;color:rgb(168 85 247 / var(--tw-text-opacity, 1))}.text-purple-600{--tw-text-opacity: 1;color:rgb(147 51 234 / var(--tw-text-opacity, 1))}.text-purple-700{--tw-text-opacity: 1;color:rgb(126 34 206 / var(--tw-text-opacity, 1))}.text-purple-900{--tw-text-opacity: 1;color:rgb(88 28 135 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.underline-offset-4{text-underline-offset:4px}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-0{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-blue-500{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.ring-yellow-200{--tw-ring-opacity: 1;--tw-ring-color: rgb(254 240 138 / var(--tw-ring-opacity, 1))}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur: blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}.duration-200{animation-duration:.2s}.duration-300{animation-duration:.3s}.ease-in-out{animation-timing-function:cubic-bezier(.4,0,.2,1)}.running{animation-play-state:running}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.last\:mb-0:last-child{margin-bottom:0}.hover\:-translate-y-1:hover{--tw-translate-y: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-2:hover{--tw-translate-y: -.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-blue-200:hover{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity, 1))}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-blue-50:hover{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.hover\:bg-destructive\/80:hover{background-color:hsl(var(--destructive) / .8)}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-primary\/80:hover{background-color:hsl(var(--primary) / .8)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-blue-700:hover{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.hover\:text-destructive:hover{color:hsl(var(--destructive))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:text-red-500:hover{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.hover\:text-red-600:hover{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-lg:hover{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-xl:hover{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:bg-accent:focus{background-color:hsl(var(--accent))}.focus\:text-accent-foreground:focus{color:hsl(var(--accent-foreground))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.focus-visible\:ring-offset-background:focus-visible{--tw-ring-offset-color: hsl(var(--background))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-70{opacity:.7}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=checked\]\:translate-x-5[data-state=checked]{--tw-translate-x: 1.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:hsl(var(--background))}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:hsl(var(--primary))}.data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:hsl(var(--input))}.data-\[state\=active\]\:text-foreground[data-state=active]{color:hsl(var(--foreground))}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.data-\[state\=open\]\:animate-in[data-state=open]{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation-name:exit;animation-duration:.15s;--tw-exit-opacity: initial;--tw-exit-scale: initial;--tw-exit-rotate: initial;--tw-exit-translate-x: initial;--tw-exit-translate-y: initial}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity: 0}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity: 0}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale: .95}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale: .95}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y: -.5rem}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x: .5rem}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x: -.5rem}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y: .5rem}.data-\[state\=closed\]\:slide-out-to-left-1\/2[data-state=closed]{--tw-exit-translate-x: -50%}.data-\[state\=closed\]\:slide-out-to-top-\[48\%\][data-state=closed]{--tw-exit-translate-y: -48%}.data-\[state\=open\]\:slide-in-from-left-1\/2[data-state=open]{--tw-enter-translate-x: -50%}.data-\[state\=open\]\:slide-in-from-top-\[48\%\][data-state=open]{--tw-enter-translate-y: -48%}@supports (backdrop-filter: var(--tw)){.supports-\[backdrop-filter\]\:bg-background\/60{background-color:hsl(var(--background) / .6)}}@media (min-width: 640px){.sm\:mt-0{margin-top:0}.sm\:inline{display:inline}.sm\:hidden{display:none}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:text-left{text-align:left}}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:static{position:static}.lg\:inset-0{top:0;right:0;bottom:0;left:0}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.\[\&\>span\]\:line-clamp-1>span{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1}
static/index.html CHANGED
@@ -5,8 +5,8 @@
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Edge LLM</title>
8
- <script type="module" crossorigin src="/assets/index-66e7a223.js"></script>
9
- <link rel="stylesheet" href="/assets/index-ed161e0d.css">
10
  </head>
11
  <body>
12
  <div id="root"></div>
 
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Edge LLM</title>
8
+ <script type="module" crossorigin src="/assets/index-6f63b786.js"></script>
9
+ <link rel="stylesheet" href="/assets/index-f766ffe0.css">
10
  </head>
11
  <body>
12
  <div id="root"></div>