wu981526092 commited on
Commit
21ed59f
·
1 Parent(s): 9c1cca5

✨ UPDATE CHAT COMPONENT AND PLAYGROUND PAGE: Refactor for improved functionality and consistency

Browse files

✅ Changes made:
- Enhanced the Chat component with better scrolling behavior and improved message rendering.
- Refactored Playground page for consistent formatting and improved state management.
- Updated various UI elements for better accessibility and user experience.

🛠️ These updates contribute to a more robust and user-friendly interface across the application!

frontend/src/components/ui/chat.tsx CHANGED
@@ -1,48 +1,78 @@
1
- import * as React from 'react'
2
- import { cn } from '@/lib/utils'
3
- import { Button } from '@/components/ui/button'
4
- import { Textarea } from '@/components/ui/textarea'
5
- import { Send, Square, User, Bot } from 'lucide-react'
6
- import ReactMarkdown from 'react-markdown'
7
 
8
- import { AssistantInfo } from '@/types/chat'
9
 
10
  export interface ChatProps extends React.HTMLAttributes<HTMLDivElement> {
11
  messages: Array<{
12
- id: string
13
- role: 'user' | 'assistant' | 'system'
14
- content: string
15
- createdAt?: Date
16
- assistantInfo?: AssistantInfo
17
- }>
18
- input: string
19
- handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
20
- handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void
21
- isGenerating?: boolean
22
- stop?: () => void
23
  }
24
 
25
  const Chat = React.forwardRef<HTMLDivElement, ChatProps>(
26
- ({ className, messages, input, handleInputChange, handleSubmit, isGenerating, stop, ...props }, ref) => {
27
- const messagesEndRef = React.useRef<HTMLDivElement>(null)
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  const scrollToBottom = () => {
30
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
31
- }
 
 
 
32
 
33
  React.useEffect(() => {
34
- console.log('Chat component - messages updated:', messages.length, messages.map(m => ({ id: m.id, role: m.role, content: m.content.slice(0, 50) + '...' })))
35
- scrollToBottom()
36
- }, [messages])
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  return (
39
  <div
40
- className={cn('flex h-full flex-col', className)}
41
  ref={ref}
42
  {...props}
43
  >
44
  {/* Messages */}
45
- <div className="flex-1 overflow-y-auto p-4 space-y-4">
 
 
 
46
  {messages.length === 0 ? (
47
  <div className="flex items-center justify-center h-full text-muted-foreground">
48
  <p>No messages yet. Start a conversation!</p>
@@ -52,88 +82,142 @@ const Chat = React.forwardRef<HTMLDivElement, ChatProps>(
52
  <div
53
  key={`${message.id}-${index}`}
54
  className={cn(
55
- 'flex gap-3 w-full',
56
- message.role === 'user' ? 'justify-end' : 'justify-start'
57
  )}
58
  >
59
  {/* Avatar for assistant */}
60
- {message.role !== 'user' && (
61
- <div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
62
  <Bot className="h-4 w-4 text-primary-foreground" />
63
  </div>
64
  )}
65
-
66
  {/* Message content */}
67
  <div
68
  className={cn(
69
- 'max-w-[75%] flex flex-col gap-2 rounded-lg px-3 py-2 text-sm',
70
- message.role === 'user'
71
- ? 'bg-primary text-primary-foreground'
72
- : 'bg-muted'
73
  )}
74
  >
75
  <div className="text-xs opacity-70 flex items-center gap-2">
76
  <span>
77
- {message.role === 'user' ? 'You' : (
78
- message.assistantInfo ? (
79
- <>
80
- <span className="font-medium">{message.assistantInfo.name}</span>
81
- {message.assistantInfo.type !== 'default' && (
82
- <span className={cn(
 
 
 
 
83
  "inline-block px-1.5 py-0.5 rounded text-[10px] font-medium ml-1",
84
- message.assistantInfo.type === 'user' && "bg-blue-100 text-blue-700",
85
- message.assistantInfo.type === 'template' && "bg-gray-100 text-gray-700",
86
- message.assistantInfo.type === 'new' && "bg-green-100 text-green-700"
87
- )}>
88
- {message.assistantInfo.type === 'user' ? 'Mine' :
89
- message.assistantInfo.type === 'template' ? 'Template' : 'New'}
90
- </span>
91
- )}
92
- {message.assistantInfo.originalTemplate && (
93
- <span className="text-[10px] text-muted-foreground">
94
- (from {message.assistantInfo.originalTemplate})
95
- </span>
96
- )}
97
- </>
98
- ) : 'Assistant'
 
 
 
 
 
 
 
 
99
  )}
100
  </span>
101
  <span>•</span>
102
  <span>#{index + 1}</span>
103
  </div>
104
- <div className="leading-relaxed prose prose-sm dark:prose-invert max-w-none">
105
  <ReactMarkdown
106
  components={{
107
  // Customize components for better styling
108
- p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
109
- ul: ({ children }) => <ul className="mb-2 last:mb-0 list-disc pl-4">{children}</ul>,
110
- ol: ({ children }) => <ol className="mb-2 last:mb-0 list-decimal pl-4">{children}</ol>,
111
- li: ({ children }) => <li className="mb-1">{children}</li>,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  code: ({ children, className }) => {
113
  const isInline = !className;
114
  return isInline ? (
115
- <code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{children}</code>
 
 
116
  ) : (
117
- <code className="block bg-muted p-2 rounded text-xs font-mono overflow-x-auto whitespace-pre">{children}</code>
118
- )
 
 
119
  },
120
- pre: ({ children }) => <div className="mb-2 last:mb-0">{children}</div>,
121
- strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
122
- em: ({ children }) => <em className="italic">{children}</em>,
123
- blockquote: ({ children }) => <blockquote className="border-l-4 border-muted pl-4 italic mb-2 last:mb-0">{children}</blockquote>,
124
- h1: ({ children }) => <h1 className="text-lg font-bold mb-2 last:mb-0">{children}</h1>,
125
- h2: ({ children }) => <h2 className="text-base font-bold mb-2 last:mb-0">{children}</h2>,
126
- h3: ({ children }) => <h3 className="text-sm font-bold mb-2 last:mb-0">{children}</h3>,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  }}
128
  >
129
  {message.content}
130
  </ReactMarkdown>
131
  </div>
132
  </div>
133
-
134
  {/* Avatar for user */}
135
- {message.role === 'user' && (
136
- <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
137
  <User className="h-4 w-4" />
138
  </div>
139
  )}
@@ -152,14 +236,19 @@ const Chat = React.forwardRef<HTMLDivElement, ChatProps>(
152
  placeholder="Type your message..."
153
  className="min-h-[60px] resize-none"
154
  onKeyDown={(e) => {
155
- if (e.key === 'Enter' && !e.shiftKey) {
156
- e.preventDefault()
157
- handleSubmit(e as any)
158
  }
159
  }}
160
  />
161
  {isGenerating ? (
162
- <Button type="button" onClick={stop} variant="outline" size="icon">
 
 
 
 
 
163
  <Square className="h-4 w-4" />
164
  </Button>
165
  ) : (
@@ -170,9 +259,9 @@ const Chat = React.forwardRef<HTMLDivElement, ChatProps>(
170
  </form>
171
  </div>
172
  </div>
173
- )
174
  }
175
- )
176
- Chat.displayName = 'Chat'
177
 
178
- export { Chat }
 
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Textarea } from "@/components/ui/textarea";
5
+ import { Send, Square, User, Bot } from "lucide-react";
6
+ import ReactMarkdown from "react-markdown";
7
 
8
+ import { AssistantInfo } from "@/types/chat";
9
 
10
  export interface ChatProps extends React.HTMLAttributes<HTMLDivElement> {
11
  messages: Array<{
12
+ id: string;
13
+ role: "user" | "assistant" | "system";
14
+ content: string;
15
+ createdAt?: Date;
16
+ assistantInfo?: AssistantInfo;
17
+ }>;
18
+ input: string;
19
+ handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
20
+ handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
21
+ isGenerating?: boolean;
22
+ stop?: () => void;
23
  }
24
 
25
  const Chat = React.forwardRef<HTMLDivElement, ChatProps>(
26
+ (
27
+ {
28
+ className,
29
+ messages,
30
+ input,
31
+ handleInputChange,
32
+ handleSubmit,
33
+ isGenerating,
34
+ stop,
35
+ ...props
36
+ },
37
+ ref
38
+ ) => {
39
+ const messagesEndRef = React.useRef<HTMLDivElement>(null);
40
+ const messagesContainerRef = React.useRef<HTMLDivElement>(null);
41
 
42
  const scrollToBottom = () => {
43
+ if (messagesContainerRef.current) {
44
+ messagesContainerRef.current.scrollTop =
45
+ messagesContainerRef.current.scrollHeight;
46
+ }
47
+ };
48
 
49
  React.useEffect(() => {
50
+ console.log(
51
+ "Chat component - messages updated:",
52
+ messages.length,
53
+ messages.map((m) => ({
54
+ id: m.id,
55
+ role: m.role,
56
+ content: m.content.slice(0, 50) + "...",
57
+ }))
58
+ );
59
+ // Use setTimeout to ensure the DOM has updated
60
+ setTimeout(() => {
61
+ scrollToBottom();
62
+ }, 100);
63
+ }, [messages]);
64
 
65
  return (
66
  <div
67
+ className={cn("flex h-full flex-col", className)}
68
  ref={ref}
69
  {...props}
70
  >
71
  {/* Messages */}
72
+ <div
73
+ ref={messagesContainerRef}
74
+ className="flex-1 overflow-y-auto p-4 space-y-4 scroll-smooth"
75
+ >
76
  {messages.length === 0 ? (
77
  <div className="flex items-center justify-center h-full text-muted-foreground">
78
  <p>No messages yet. Start a conversation!</p>
 
82
  <div
83
  key={`${message.id}-${index}`}
84
  className={cn(
85
+ "flex gap-3 w-full items-start",
86
+ message.role === "user" ? "justify-end" : "justify-start"
87
  )}
88
  >
89
  {/* Avatar for assistant */}
90
+ {message.role !== "user" && (
91
+ <div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center flex-shrink-0 mt-1">
92
  <Bot className="h-4 w-4 text-primary-foreground" />
93
  </div>
94
  )}
95
+
96
  {/* Message content */}
97
  <div
98
  className={cn(
99
+ "max-w-[75%] min-w-0 flex flex-col gap-2 rounded-lg px-3 py-2 text-sm break-words",
100
+ message.role === "user"
101
+ ? "bg-primary text-primary-foreground"
102
+ : "bg-muted"
103
  )}
104
  >
105
  <div className="text-xs opacity-70 flex items-center gap-2">
106
  <span>
107
+ {message.role === "user" ? (
108
+ "You"
109
+ ) : message.assistantInfo ? (
110
+ <>
111
+ <span className="font-medium">
112
+ {message.assistantInfo.name}
113
+ </span>
114
+ {message.assistantInfo.type !== "default" && (
115
+ <span
116
+ className={cn(
117
  "inline-block px-1.5 py-0.5 rounded text-[10px] font-medium ml-1",
118
+ message.assistantInfo.type === "user" &&
119
+ "bg-blue-100 text-blue-700",
120
+ message.assistantInfo.type === "template" &&
121
+ "bg-gray-100 text-gray-700",
122
+ message.assistantInfo.type === "new" &&
123
+ "bg-green-100 text-green-700"
124
+ )}
125
+ >
126
+ {message.assistantInfo.type === "user"
127
+ ? "Mine"
128
+ : message.assistantInfo.type === "template"
129
+ ? "Template"
130
+ : "New"}
131
+ </span>
132
+ )}
133
+ {message.assistantInfo.originalTemplate && (
134
+ <span className="text-[10px] text-muted-foreground">
135
+ (from {message.assistantInfo.originalTemplate})
136
+ </span>
137
+ )}
138
+ </>
139
+ ) : (
140
+ "Assistant"
141
  )}
142
  </span>
143
  <span>•</span>
144
  <span>#{index + 1}</span>
145
  </div>
146
+ <div className="leading-relaxed prose prose-sm dark:prose-invert max-w-none overflow-hidden">
147
  <ReactMarkdown
148
  components={{
149
  // Customize components for better styling
150
+ p: ({ children }) => (
151
+ <p className="mb-2 last:mb-0 break-words">
152
+ {children}
153
+ </p>
154
+ ),
155
+ ul: ({ children }) => (
156
+ <ul className="mb-2 last:mb-0 list-disc pl-4 break-words">
157
+ {children}
158
+ </ul>
159
+ ),
160
+ ol: ({ children }) => (
161
+ <ol className="mb-2 last:mb-0 list-decimal pl-4 break-words">
162
+ {children}
163
+ </ol>
164
+ ),
165
+ li: ({ children }) => (
166
+ <li className="mb-1 break-words">{children}</li>
167
+ ),
168
  code: ({ children, className }) => {
169
  const isInline = !className;
170
  return isInline ? (
171
+ <code className="bg-muted px-1 py-0.5 rounded text-xs font-mono break-words">
172
+ {children}
173
+ </code>
174
  ) : (
175
+ <code className="block bg-muted p-2 rounded text-xs font-mono overflow-x-auto whitespace-pre-wrap break-all">
176
+ {children}
177
+ </code>
178
+ );
179
  },
180
+ pre: ({ children }) => (
181
+ <div className="mb-2 last:mb-0 overflow-hidden">
182
+ {children}
183
+ </div>
184
+ ),
185
+ strong: ({ children }) => (
186
+ <strong className="font-semibold">{children}</strong>
187
+ ),
188
+ em: ({ children }) => (
189
+ <em className="italic">{children}</em>
190
+ ),
191
+ blockquote: ({ children }) => (
192
+ <blockquote className="border-l-4 border-muted pl-4 italic mb-2 last:mb-0">
193
+ {children}
194
+ </blockquote>
195
+ ),
196
+ h1: ({ children }) => (
197
+ <h1 className="text-lg font-bold mb-2 last:mb-0">
198
+ {children}
199
+ </h1>
200
+ ),
201
+ h2: ({ children }) => (
202
+ <h2 className="text-base font-bold mb-2 last:mb-0">
203
+ {children}
204
+ </h2>
205
+ ),
206
+ h3: ({ children }) => (
207
+ <h3 className="text-sm font-bold mb-2 last:mb-0">
208
+ {children}
209
+ </h3>
210
+ ),
211
  }}
212
  >
213
  {message.content}
214
  </ReactMarkdown>
215
  </div>
216
  </div>
217
+
218
  {/* Avatar for user */}
219
+ {message.role === "user" && (
220
+ <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0 mt-1">
221
  <User className="h-4 w-4" />
222
  </div>
223
  )}
 
236
  placeholder="Type your message..."
237
  className="min-h-[60px] resize-none"
238
  onKeyDown={(e) => {
239
+ if (e.key === "Enter" && !e.shiftKey) {
240
+ e.preventDefault();
241
+ handleSubmit(e as any);
242
  }
243
  }}
244
  />
245
  {isGenerating ? (
246
+ <Button
247
+ type="button"
248
+ onClick={stop}
249
+ variant="outline"
250
+ size="icon"
251
+ >
252
  <Square className="h-4 w-4" />
253
  </Button>
254
  ) : (
 
259
  </form>
260
  </div>
261
  </div>
262
+ );
263
  }
264
+ );
265
+ Chat.displayName = "Chat";
266
 
267
+ export { Chat };
frontend/src/pages/Playground.tsx CHANGED
@@ -1,10 +1,10 @@
1
- import { useState, useEffect } from 'react'
2
- import { AssistantInfo } from '@/types/chat'
3
- import { getPresetsFromConfigs } from '@/config/assistants'
4
- import { Button } from '@/components/ui/button'
5
- import { Card } from '@/components/ui/card'
6
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
7
- import { Label } from '@/components/ui/label'
8
 
9
  import {
10
  AlertDialog,
@@ -15,10 +15,10 @@ import {
15
  AlertDialogFooter,
16
  AlertDialogHeader,
17
  AlertDialogTitle,
18
- } from '@/components/ui/alert-dialog'
19
- import { Chat } from '@/components/ui/chat'
20
- import { useChat } from '@/hooks/useChat'
21
- import {
22
  Plus,
23
  Trash2,
24
  Save,
@@ -27,30 +27,30 @@ import {
27
  BookOpen,
28
  MessageSquare,
29
  ChevronLeft,
30
- ChevronRight
31
- } from 'lucide-react'
32
 
33
  // Import refactored components
34
- import {
35
- ModelParametersTab,
36
- AssistantSelector,
37
- SystemInstructionsTab,
38
- DocumentsTab
39
- } from '@/components/playground'
40
 
41
  interface ModelInfo {
42
- model_name: string
43
- name: string
44
- supports_thinking: boolean
45
- description: string
46
- size_gb: string
47
- is_loaded: boolean
48
- type: 'local' | 'api'
49
  }
50
 
51
  interface ModelsResponse {
52
- models: ModelInfo[]
53
- current_model: string
54
  }
55
 
56
  export function Playground() {
@@ -74,68 +74,76 @@ export function Playground() {
74
  temperature,
75
  setTemperature,
76
  maxTokens,
77
- setMaxTokens
78
- } = useChat()
79
 
80
  // RAG configuration state
81
- const [ragEnabled, setRagEnabled] = useState(false)
82
- const [retrievalCount, setRetrievalCount] = useState(3)
83
 
84
  // UI state - sidebar collapse states
85
- const [sessionsCollapsed, setSessionsCollapsed] = useState(false)
86
- const [configCollapsed, setConfigCollapsed] = useState(false)
87
- const [autoLoadingModel, setAutoLoadingModel] = useState<string | null>(null)
88
- const [showLoadConfirm, setShowLoadConfirm] = useState(false)
89
- const [pendingModelToLoad, setPendingModelToLoad] = useState<ModelInfo | null>(null)
90
-
 
91
  // Model management state
92
- const [models, setModels] = useState<ModelInfo[]>([])
93
 
94
  // Saved assistants state
95
- const [savedAssistants, setSavedAssistants] = useState<any[]>([])
96
- const [selectedAssistant, setSelectedAssistant] = useState<{id: string, name: string, type: 'user'|'template'|'new', originalTemplate?: string} | null>(null)
97
-
 
 
 
 
 
98
  // Save assistant dialog state
99
- const [showSaveDialog, setShowSaveDialog] = useState(false)
100
- const [saveAssistantName, setSaveAssistantName] = useState('')
101
-
102
  // Rename assistant dialog state
103
- const [showRenameDialog, setShowRenameDialog] = useState(false)
104
- const [renameAssistantName, setRenameAssistantName] = useState('')
105
 
106
  // Load saved assistants
107
  const loadSavedAssistants = () => {
108
  try {
109
- const assistants = JSON.parse(localStorage.getItem('savedAssistants') || '[]')
110
- setSavedAssistants(assistants)
 
 
111
  } catch (error) {
112
- console.error('Failed to load saved assistants:', error)
113
  }
114
- }
115
 
116
  // Open save assistant dialog
117
  const openSaveDialog = () => {
118
- if (!systemPrompt.trim()) return
119
-
120
  // Set default name based on selected assistant or create a default
121
- let defaultName = 'My Assistant'
122
  if (selectedAssistant) {
123
- if (selectedAssistant.type === 'new') {
124
- defaultName = selectedAssistant.name
125
- } else if (selectedAssistant.type === 'template') {
126
- defaultName = `My ${selectedAssistant.name}`
127
  } else {
128
- defaultName = selectedAssistant.name + ' Copy'
129
  }
130
  }
131
-
132
- setSaveAssistantName(defaultName)
133
- setShowSaveDialog(true)
134
- }
135
 
136
  // Actually save the assistant with user-defined name
137
  const confirmSaveAssistant = () => {
138
- if (!saveAssistantName.trim() || !systemPrompt.trim()) return
139
 
140
  const newAssistant = {
141
  id: Date.now().toString(),
@@ -147,391 +155,421 @@ export function Playground() {
147
  ragEnabled,
148
  retrievalCount,
149
  documents: [], // Assistant-specific documents (future: populated from DocumentsTab)
150
- createdAt: new Date().toISOString()
151
- }
 
 
 
 
152
 
153
- const updatedAssistants = [...savedAssistants, newAssistant]
154
- setSavedAssistants(updatedAssistants)
155
- localStorage.setItem('savedAssistants', JSON.stringify(updatedAssistants))
156
-
157
  // Update current selection to the newly saved assistant
158
  setSelectedAssistant({
159
  id: newAssistant.id,
160
  name: newAssistant.name,
161
- type: 'user'
162
- })
163
-
164
  // Close dialog
165
- setShowSaveDialog(false)
166
- setSaveAssistantName('')
167
- }
168
 
169
  // Cancel save dialog
170
  const cancelSaveDialog = () => {
171
- setShowSaveDialog(false)
172
- setSaveAssistantName('')
173
- }
174
 
175
  // Load saved assistant
176
  const loadSavedAssistant = (assistantId: string) => {
177
- const assistant = savedAssistants.find(a => a.id === assistantId)
178
  if (assistant) {
179
- setSystemPrompt(assistant.systemPrompt)
180
- setTemperature(assistant.temperature)
181
- setMaxTokens(assistant.maxTokens)
182
  // Load RAG settings with defaults for backwards compatibility
183
- setRagEnabled(assistant.ragEnabled || false)
184
- setRetrievalCount(assistant.retrievalCount || 3)
185
  if (assistant.model) {
186
- setSelectedModel(assistant.model)
187
  }
188
  setSelectedAssistant({
189
  id: assistant.id,
190
  name: assistant.name,
191
- type: 'user'
192
- })
193
  }
194
- }
195
 
196
  // Preset system prompts from shared config
197
- const systemPromptPresets = getPresetsFromConfigs()
198
 
199
  // Handle preset selection
200
  const handlePresetSelect = (presetName: string) => {
201
- const preset = systemPromptPresets.find(p => p.name === presetName)
202
  if (preset) {
203
- setSystemPrompt(preset.prompt)
204
  // Reset RAG settings to defaults for templates
205
- setRagEnabled(false)
206
- setRetrievalCount(3)
207
  setSelectedAssistant({
208
  id: presetName,
209
  name: preset.name,
210
- type: 'template'
211
- })
212
  }
213
- }
214
 
215
  // Create new assistant (clear all settings but keep current session)
216
  const createNewAssistant = () => {
217
- setSystemPrompt('')
218
- setTemperature(0.7)
219
- setMaxTokens(1024)
220
  // Reset RAG settings to defaults
221
- setRagEnabled(false)
222
- setRetrievalCount(3)
223
  setSelectedAssistant({
224
- id: 'new_assistant',
225
- name: 'New Assistant',
226
- type: 'new'
227
- })
228
  // Keep current session - only clear assistant settings
229
- }
230
 
231
  // Clear current assistant
232
  const clearCurrentAssistant = () => {
233
- setSelectedAssistant(null)
234
- }
235
 
236
  // Open rename assistant dialog
237
  const openRenameDialog = () => {
238
  if (selectedAssistant) {
239
- setRenameAssistantName(selectedAssistant.name)
240
- setShowRenameDialog(true)
241
  }
242
- }
243
 
244
  // Confirm rename assistant
245
  const confirmRenameAssistant = () => {
246
- if (!selectedAssistant || !renameAssistantName.trim()) return
247
-
248
  // Update selectedAssistant state
249
  const updatedAssistant = {
250
  ...selectedAssistant,
251
- name: renameAssistantName.trim()
252
- }
253
- setSelectedAssistant(updatedAssistant)
254
-
255
  // If it's a saved user assistant, update localStorage
256
- if (selectedAssistant.type === 'user') {
257
- const updatedAssistants = savedAssistants.map(assistant =>
258
- assistant.id === selectedAssistant.id
259
  ? { ...assistant, name: renameAssistantName.trim() }
260
  : assistant
261
- )
262
- setSavedAssistants(updatedAssistants)
263
- localStorage.setItem('savedAssistants', JSON.stringify(updatedAssistants))
 
 
 
264
  }
265
-
266
  // Close dialog
267
- setShowRenameDialog(false)
268
- setRenameAssistantName('')
269
- }
270
 
271
  // Cancel rename dialog
272
  const cancelRenameDialog = () => {
273
- setShowRenameDialog(false)
274
- setRenameAssistantName('')
275
- }
276
 
277
  // Get current assistant information
278
  const getCurrentAssistantInfo = (): AssistantInfo => {
279
  return {
280
- name: selectedAssistant?.name || 'Default Assistant',
281
- type: selectedAssistant?.type || 'default',
282
  systemPrompt,
283
  temperature,
284
  maxTokens,
285
- originalTemplate: selectedAssistant?.originalTemplate
286
- }
287
- }
288
 
289
  // Convert template to new assistant when settings change
290
  const convertTemplateToNew = () => {
291
- if (selectedAssistant && selectedAssistant.type === 'template') {
292
  setSelectedAssistant({
293
- id: 'new_assistant',
294
  name: `Custom ${selectedAssistant.name}`,
295
- type: 'new',
296
- originalTemplate: selectedAssistant.name
297
- })
298
  }
299
- }
300
 
301
  // Wrapped setters that trigger template conversion
302
  const handleSystemPromptChange = (prompt: string) => {
303
- setSystemPrompt(prompt)
304
- convertTemplateToNew()
305
- }
306
 
307
  const handleTemperatureChange = (temp: number) => {
308
- setTemperature(temp)
309
- convertTemplateToNew()
310
- }
311
 
312
  const handleMaxTokensChange = (tokens: number) => {
313
- setMaxTokens(tokens)
314
- convertTemplateToNew()
315
- }
316
 
317
  const handleRagEnabledChange = (enabled: boolean) => {
318
- setRagEnabled(enabled)
319
- convertTemplateToNew()
320
- }
321
 
322
  const handleRetrievalCountChange = (count: number) => {
323
- setRetrievalCount(count)
324
- convertTemplateToNew()
325
- }
326
 
327
  // Load available models and saved assistants on startup
328
  useEffect(() => {
329
- fetchModels()
330
- loadSavedAssistants()
331
-
332
  // Check if there's a template configuration to load
333
- const loadConfig = localStorage.getItem('loadAssistantConfig')
334
  if (loadConfig) {
335
  try {
336
- const config = JSON.parse(loadConfig)
337
- setSystemPrompt(config.systemPrompt || '')
338
- setTemperature(config.temperature || 0.7)
339
- setMaxTokens(config.maxTokens || 1024)
340
  if (config.model) {
341
- setSelectedModel(config.model)
342
  }
343
  setSelectedAssistant({
344
- id: 'loaded_template',
345
- name: config.name || 'Loaded Template',
346
- type: 'template'
347
- })
348
-
349
  // Clear the config after loading
350
- localStorage.removeItem('loadAssistantConfig')
351
  } catch (error) {
352
- console.error('Failed to load assistant config:', error)
353
- localStorage.removeItem('loadAssistantConfig')
354
  }
355
  }
356
- }, [])
357
 
358
  // Debug logs for Session issue
359
  useEffect(() => {
360
- console.log('Sidebar states:', { sessionsCollapsed, configCollapsed, sessionsCount: sessions.length, currentSessionId })
361
- }, [sessionsCollapsed, configCollapsed, sessions.length, currentSessionId])
 
 
 
 
 
362
 
363
  useEffect(() => {
364
- console.log('Rendering sessions:', sessions.length, sessions.map(s => ({id: s.id, title: s.title})))
365
- }, [sessions])
 
 
 
 
366
 
367
  // Update selected model when models change
368
  useEffect(() => {
369
  // Only reset if the selected model no longer exists in the models list
370
- if (selectedModel && !models.find(m => m.model_name === selectedModel)) {
371
- const firstModel = models[0]
372
  if (firstModel) {
373
- setSelectedModel(firstModel.model_name)
374
  }
375
  }
376
- }, [models, selectedModel, setSelectedModel])
377
 
378
  // Auto-load/unload local models when selection changes
379
  useEffect(() => {
380
  const handleModelChange = async () => {
381
- if (!selectedModel || !models.length) return
382
 
383
- const selectedModelInfo = models.find(m => m.model_name === selectedModel)
384
- if (!selectedModelInfo) return
 
 
385
 
386
- const baseUrl = `${window.location.protocol}//${window.location.host}`
387
 
388
  // If selected model is a local model and not loaded, show confirmation
389
- if (selectedModelInfo.type === 'local' && !selectedModelInfo.is_loaded) {
390
- setPendingModelToLoad(selectedModelInfo)
391
- setShowLoadConfirm(true)
392
- return // Don't auto-load, wait for user confirmation
393
  }
394
 
395
  // Unload other local models that are loaded but not selected
396
- const loadedLocalModels = models.filter(m =>
397
- m.type === 'local' &&
398
- m.is_loaded &&
399
- m.model_name !== selectedModel
400
- )
401
 
402
  for (const model of loadedLocalModels) {
403
  try {
404
  const response = await fetch(`${baseUrl}/unload-model`, {
405
- method: 'POST',
406
- headers: { 'Content-Type': 'application/json' },
407
- body: JSON.stringify({ model_name: model.model_name })
408
- })
409
-
410
  if (response.ok) {
411
- console.log(`✅ Auto-unloaded local model: ${model.model_name}`)
412
  }
413
  } catch (error) {
414
- console.error(`Error auto-unloading model ${model.model_name}:`, error)
 
 
 
415
  }
416
  }
417
-
418
  // Refresh models after any unloading
419
  if (loadedLocalModels.length > 0) {
420
- fetchModels()
421
  }
422
- }
423
 
424
- handleModelChange()
425
- }, [selectedModel, models])
426
 
427
  const handleLoadModelConfirm = async () => {
428
- if (!pendingModelToLoad) return
429
-
430
- setShowLoadConfirm(false)
431
- setAutoLoadingModel(pendingModelToLoad.model_name)
432
-
433
  try {
434
- const baseUrl = `${window.location.protocol}//${window.location.host}`
435
  const response = await fetch(`${baseUrl}/load-model`, {
436
- method: 'POST',
437
- headers: { 'Content-Type': 'application/json' },
438
- body: JSON.stringify({ model_name: pendingModelToLoad.model_name })
439
- })
440
-
441
  if (response.ok) {
442
- console.log(`✅ User confirmed and loaded: ${pendingModelToLoad.model_name}`)
443
- fetchModels() // Refresh model states
 
 
444
  } else {
445
- console.error(`❌ Failed to load model: ${pendingModelToLoad.model_name}`)
 
 
446
  // Revert to an API model if load failed
447
- const apiModel = models.find(m => m.type === 'api')
448
  if (apiModel) {
449
- setSelectedModel(apiModel.model_name)
450
  }
451
  }
452
  } catch (error) {
453
- console.error('Error loading model:', error)
454
  // Revert to an API model if error
455
- const apiModel = models.find(m => m.type === 'api')
456
  if (apiModel) {
457
- setSelectedModel(apiModel.model_name)
458
  }
459
  } finally {
460
- setAutoLoadingModel(null)
461
- setPendingModelToLoad(null)
462
  }
463
- }
464
 
465
  const handleLoadModelCancel = () => {
466
- setShowLoadConfirm(false)
467
- setPendingModelToLoad(null)
468
-
469
  // Revert to an API model
470
- const apiModel = models.find(m => m.type === 'api')
471
  if (apiModel) {
472
- setSelectedModel(apiModel.model_name)
473
  }
474
- }
475
 
476
  // Cleanup: unload all local models when component unmounts or user leaves
477
  useEffect(() => {
478
  const handlePageUnload = async () => {
479
- const baseUrl = `${window.location.protocol}//${window.location.host}`
480
- const loadedLocalModels = models.filter(m => m.type === 'local' && m.is_loaded)
481
-
 
 
482
  for (const model of loadedLocalModels) {
483
  try {
484
  await fetch(`${baseUrl}/unload-model`, {
485
- method: 'POST',
486
- headers: { 'Content-Type': 'application/json' },
487
- body: JSON.stringify({ model_name: model.model_name })
488
- })
489
- console.log(`✅ Cleanup: unloaded ${model.model_name}`)
490
  } catch (error) {
491
- console.error(`Error cleaning up model ${model.model_name}:`, error)
492
  }
493
  }
494
- }
495
 
496
  // Cleanup on component unmount
497
  return () => {
498
- handlePageUnload()
499
- }
500
- }, [models])
501
 
502
  const fetchModels = async () => {
503
  try {
504
- const baseUrl = `${window.location.protocol}//${window.location.host}`
505
- const res = await fetch(`${baseUrl}/models`)
506
  if (res.ok) {
507
- const data: ModelsResponse = await res.json()
508
- setModels(data.models)
509
-
510
  // Set selected model to current model if available, otherwise first API model
511
  if (data.current_model && selectedModel !== data.current_model) {
512
- setSelectedModel(data.current_model)
513
  } else if (!selectedModel && data.models.length > 0) {
514
  // Prefer API models as default
515
- const apiModel = data.models.find(m => m.type === 'api')
516
- const defaultModel = apiModel || data.models[0]
517
- setSelectedModel(defaultModel.model_name)
518
  }
519
  }
520
  } catch (err) {
521
- console.error('Failed to fetch models:', err)
522
  }
523
- }
524
 
525
  return (
526
  <div className="h-screen bg-background flex">
527
  {/* Chat Sessions Sidebar */}
528
- <div className={`
 
529
  bg-background border-r flex-shrink-0 transition-all duration-300 ease-in-out
530
- ${sessionsCollapsed ? 'w-12' : 'w-80'}
531
- `}>
 
532
  <div className="p-4 space-y-4 h-full">
533
- <div className={`flex items-center ${sessionsCollapsed ? 'justify-center' : 'justify-between'}`}>
534
- {!sessionsCollapsed && <h2 className="font-semibold">Chat Sessions</h2>}
 
 
 
 
 
 
535
  <div className="flex gap-1">
536
  {!sessionsCollapsed && (
537
  <Button onClick={clearCurrentSession} size="sm">
@@ -539,76 +577,102 @@ export function Playground() {
539
  New
540
  </Button>
541
  )}
542
- <Button
543
- onClick={() => setSessionsCollapsed(!sessionsCollapsed)}
544
- size="sm"
545
  variant="ghost"
546
  className="h-8 w-8 p-0 hover:bg-gray-100"
547
- title={sessionsCollapsed ? "Expand Sessions" : "Collapse Sessions"}
 
 
548
  >
549
- {sessionsCollapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
 
 
 
 
550
  </Button>
551
  </div>
552
  </div>
553
-
554
  {sessionsCollapsed && (
555
- <Button onClick={clearCurrentSession} size="sm" variant="ghost" className="w-full p-2">
 
 
 
 
 
556
  <Plus className="h-4 w-4" />
557
  </Button>
558
  )}
559
  <div className="space-y-2">
560
-
561
- {!sessionsCollapsed && sessions.map((session) => (
562
- <Card
563
- key={session.id}
564
- className={`p-3 cursor-pointer transition-colors hover:bg-accent ${
565
- currentSessionId === session.id ? 'bg-accent border-primary' : ''
566
- }`}
567
- onClick={() => {
568
- console.log('Session card clicked:', session.id, session.title)
569
- selectSession(session.id)
570
- }}
571
- >
572
- <div className="flex items-center justify-between">
573
- <span className="text-sm font-medium truncate">{session.title}</span>
574
- <Button
575
- size="sm"
576
- variant="ghost"
577
- onClick={(e) => {
578
- e.stopPropagation()
579
- deleteSession(session.id)
580
- }}
581
- className="h-6 w-6 p-0"
582
- >
583
- <Trash2 className="h-3 w-3" />
584
- </Button>
585
- </div>
586
- <div className="text-xs text-muted-foreground">
587
- {session.messages.length} messages
588
- </div>
589
- </Card>
590
- ))}
591
-
592
- {sessionsCollapsed && sessions.map((session) => (
593
- <Button
594
- key={session.id}
595
- variant={currentSessionId === session.id ? "default" : "ghost"}
596
- size="sm"
597
- className="w-full p-1 h-8 relative"
598
- title={session.title}
599
- onClick={() => {
600
- console.log('Session icon clicked:', session.id, session.title)
601
- selectSession(session.id)
602
- }}
603
- >
604
- <MessageSquare className="h-4 w-4" />
605
- {session.messages && session.messages.length > 0 && (
606
- <div className="absolute -top-1 -right-1 w-3 h-3 bg-blue-500 text-white text-xs rounded-full flex items-center justify-center">
607
- {Math.min(session.messages.length, 9)}
608
  </div>
609
- )}
610
- </Button>
611
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  </div>
613
  </div>
614
  </div>
@@ -621,21 +685,25 @@ export function Playground() {
621
  <div className="flex-1 flex flex-col">
622
  {/* Chat Messages and Input */}
623
  <Chat
624
- messages={messages.map(msg => ({
625
  id: msg.id,
626
- role: msg.role as 'user' | 'assistant' | 'system',
627
  content: msg.content,
628
  createdAt: new Date(msg.timestamp),
629
- assistantInfo: msg.assistantInfo
630
  }))}
631
  input={input}
632
  handleInputChange={(e) => setInput(e.target.value)}
633
  handleSubmit={async (e) => {
634
- e.preventDefault()
635
- if (!selectedModel || !models.find(m => m.model_name === selectedModel)) return
636
- const assistantInfo = getCurrentAssistantInfo()
637
- const ragConfig = { useRag: ragEnabled, retrievalCount }
638
- await sendMessage(assistantInfo, ragConfig)
 
 
 
 
639
  }}
640
  isGenerating={isLoading}
641
  stop={stopGeneration}
@@ -644,28 +712,48 @@ export function Playground() {
644
  </div>
645
 
646
  {/* Settings Panel - Tabbed Configuration Sidebar */}
647
- <div className={`
 
648
  border-l bg-background flex-shrink-0 transition-all duration-300 ease-in-out
649
- ${configCollapsed ? 'w-12' : 'w-[480px] xl:w-[520px]'}
650
- `}>
 
651
  <div className="p-4 space-y-4 h-full">
652
- <div className={`flex items-center ${configCollapsed ? 'justify-center' : 'justify-between'}`}>
653
- {!configCollapsed && <h2 className="font-semibold">Configuration</h2>}
 
 
 
 
 
 
654
  <div className="flex gap-1">
655
- <Button
656
- onClick={() => setConfigCollapsed(!configCollapsed)}
657
- size="sm"
658
  variant="ghost"
659
  className="h-8 w-8 p-0 hover:bg-gray-100"
660
- title={configCollapsed ? "Expand Configuration" : "Collapse Configuration"}
 
 
 
 
661
  >
662
- {configCollapsed ? <ChevronLeft className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
 
 
 
 
663
  </Button>
664
  </div>
665
  </div>
666
-
667
  {configCollapsed && (
668
- <Button size="sm" variant="ghost" className="w-full p-2 invisible">
 
 
 
 
669
  <Settings className="h-4 w-4" />
670
  </Button>
671
  )}
@@ -674,7 +762,7 @@ export function Playground() {
674
  <>
675
  {/* Assistant Selection Section */}
676
  <div className="border-b bg-white p-4 -mx-4">
677
- <AssistantSelector
678
  savedAssistants={savedAssistants}
679
  loadSavedAssistant={loadSavedAssistant}
680
  openSaveDialog={openSaveDialog}
@@ -689,56 +777,77 @@ export function Playground() {
689
  />
690
  </div>
691
 
692
- <Tabs defaultValue="parameters" className="flex-1 flex flex-col -mx-4">
693
- <TabsList className="grid w-full grid-cols-3 m-4 mb-0">
694
- <TabsTrigger value="parameters" className="flex items-center gap-2">
695
- <Sliders className="h-4 w-4" />
696
- <span className="hidden sm:inline">Parameters</span>
697
- </TabsTrigger>
698
- <TabsTrigger value="instructions" className="flex items-center gap-2">
699
- <Settings className="h-4 w-4" />
700
- <span className="hidden sm:inline">Instructions</span>
701
- </TabsTrigger>
702
- <TabsTrigger value="documents" className="flex items-center gap-2">
703
- <BookOpen className="h-4 w-4" />
704
- <span className="hidden sm:inline">Documents</span>
705
- </TabsTrigger>
706
- </TabsList>
707
-
708
- <div className="flex-1 overflow-hidden">
709
- <TabsContent value="parameters" className="p-6 space-y-6 m-0 h-full overflow-y-auto">
710
- <ModelParametersTab
711
- models={models}
712
- selectedModel={selectedModel}
713
- setSelectedModel={setSelectedModel}
714
- autoLoadingModel={autoLoadingModel}
715
- temperature={temperature}
716
- setTemperature={handleTemperatureChange}
717
- maxTokens={maxTokens}
718
- setMaxTokens={handleMaxTokensChange}
719
- />
720
- </TabsContent>
721
-
722
- <TabsContent value="instructions" className="p-6 space-y-6 m-0 h-full overflow-y-auto">
723
- <SystemInstructionsTab
724
- systemPrompt={systemPrompt}
725
- setSystemPrompt={handleSystemPromptChange}
726
- isLoading={isLoading}
727
- />
728
- </TabsContent>
729
-
730
- <TabsContent value="documents" className="p-6 space-y-6 m-0 h-full overflow-y-auto">
731
- <DocumentsTab
732
- isLoading={isLoading}
733
- ragEnabled={ragEnabled}
734
- setRagEnabled={handleRagEnabledChange}
735
- retrievalCount={retrievalCount}
736
- setRetrievalCount={handleRetrievalCountChange}
737
- currentAssistant={selectedAssistant}
738
- />
739
- </TabsContent>
740
- </div>
741
- </Tabs>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
742
  </>
743
  )}
744
  </div>
@@ -753,8 +862,10 @@ export function Playground() {
753
  <AlertDialogHeader>
754
  <AlertDialogTitle>Load Local Model</AlertDialogTitle>
755
  <AlertDialogDescription>
756
- Loading <strong>{pendingModelToLoad?.name}</strong> will use approximately <strong>{pendingModelToLoad?.size_gb}</strong> of RAM and storage.
757
- First-time loading may require downloading the model.
 
 
758
  </AlertDialogDescription>
759
  </AlertDialogHeader>
760
  <AlertDialogFooter>
@@ -774,7 +885,8 @@ export function Playground() {
774
  <AlertDialogHeader>
775
  <AlertDialogTitle>Save Assistant</AlertDialogTitle>
776
  <AlertDialogDescription>
777
- Give your assistant a custom name. This will be displayed in your assistant list.
 
778
  </AlertDialogDescription>
779
  </AlertDialogHeader>
780
  <div className="py-4">
@@ -791,11 +903,11 @@ export function Playground() {
791
  maxLength={100}
792
  autoFocus
793
  onKeyDown={(e) => {
794
- if (e.key === 'Enter' && saveAssistantName.trim()) {
795
- confirmSaveAssistant()
796
  }
797
- if (e.key === 'Escape') {
798
- cancelSaveDialog()
799
  }
800
  }}
801
  />
@@ -807,7 +919,7 @@ export function Playground() {
807
  <AlertDialogCancel onClick={cancelSaveDialog}>
808
  Cancel
809
  </AlertDialogCancel>
810
- <AlertDialogAction
811
  onClick={confirmSaveAssistant}
812
  disabled={!saveAssistantName.trim()}
813
  >
@@ -828,7 +940,10 @@ export function Playground() {
828
  </AlertDialogDescription>
829
  </AlertDialogHeader>
830
  <div className="py-4">
831
- <Label htmlFor="rename-assistant-name" className="text-sm font-medium">
 
 
 
832
  Assistant Name
833
  </Label>
834
  <input
@@ -841,11 +956,11 @@ export function Playground() {
841
  maxLength={100}
842
  autoFocus
843
  onKeyDown={(e) => {
844
- if (e.key === 'Enter' && renameAssistantName.trim()) {
845
- confirmRenameAssistant()
846
  }
847
- if (e.key === 'Escape') {
848
- cancelRenameDialog()
849
  }
850
  }}
851
  />
@@ -857,7 +972,7 @@ export function Playground() {
857
  <AlertDialogCancel onClick={cancelRenameDialog}>
858
  Cancel
859
  </AlertDialogCancel>
860
- <AlertDialogAction
861
  onClick={confirmRenameAssistant}
862
  disabled={!renameAssistantName.trim()}
863
  >
@@ -868,5 +983,5 @@ export function Playground() {
868
  </AlertDialogContent>
869
  </AlertDialog>
870
  </div>
871
- )
872
  }
 
1
+ import { useState, useEffect } from "react";
2
+ import { AssistantInfo } from "@/types/chat";
3
+ import { getPresetsFromConfigs } from "@/config/assistants";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Card } from "@/components/ui/card";
6
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
7
+ import { Label } from "@/components/ui/label";
8
 
9
  import {
10
  AlertDialog,
 
15
  AlertDialogFooter,
16
  AlertDialogHeader,
17
  AlertDialogTitle,
18
+ } from "@/components/ui/alert-dialog";
19
+ import { Chat } from "@/components/ui/chat";
20
+ import { useChat } from "@/hooks/useChat";
21
+ import {
22
  Plus,
23
  Trash2,
24
  Save,
 
27
  BookOpen,
28
  MessageSquare,
29
  ChevronLeft,
30
+ ChevronRight,
31
+ } from "lucide-react";
32
 
33
  // Import refactored components
34
+ import {
35
+ ModelParametersTab,
36
+ AssistantSelector,
37
+ SystemInstructionsTab,
38
+ DocumentsTab,
39
+ } from "@/components/playground";
40
 
41
  interface ModelInfo {
42
+ model_name: string;
43
+ name: string;
44
+ supports_thinking: boolean;
45
+ description: string;
46
+ size_gb: string;
47
+ is_loaded: boolean;
48
+ type: "local" | "api";
49
  }
50
 
51
  interface ModelsResponse {
52
+ models: ModelInfo[];
53
+ current_model: string;
54
  }
55
 
56
  export function Playground() {
 
74
  temperature,
75
  setTemperature,
76
  maxTokens,
77
+ setMaxTokens,
78
+ } = useChat();
79
 
80
  // RAG configuration state
81
+ const [ragEnabled, setRagEnabled] = useState(false);
82
+ const [retrievalCount, setRetrievalCount] = useState(3);
83
 
84
  // UI state - sidebar collapse states
85
+ const [sessionsCollapsed, setSessionsCollapsed] = useState(false);
86
+ const [configCollapsed, setConfigCollapsed] = useState(false);
87
+ const [autoLoadingModel, setAutoLoadingModel] = useState<string | null>(null);
88
+ const [showLoadConfirm, setShowLoadConfirm] = useState(false);
89
+ const [pendingModelToLoad, setPendingModelToLoad] =
90
+ useState<ModelInfo | null>(null);
91
+
92
  // Model management state
93
+ const [models, setModels] = useState<ModelInfo[]>([]);
94
 
95
  // Saved assistants state
96
+ const [savedAssistants, setSavedAssistants] = useState<any[]>([]);
97
+ const [selectedAssistant, setSelectedAssistant] = useState<{
98
+ id: string;
99
+ name: string;
100
+ type: "user" | "template" | "new";
101
+ originalTemplate?: string;
102
+ } | null>(null);
103
+
104
  // Save assistant dialog state
105
+ const [showSaveDialog, setShowSaveDialog] = useState(false);
106
+ const [saveAssistantName, setSaveAssistantName] = useState("");
107
+
108
  // Rename assistant dialog state
109
+ const [showRenameDialog, setShowRenameDialog] = useState(false);
110
+ const [renameAssistantName, setRenameAssistantName] = useState("");
111
 
112
  // Load saved assistants
113
  const loadSavedAssistants = () => {
114
  try {
115
+ const assistants = JSON.parse(
116
+ localStorage.getItem("savedAssistants") || "[]"
117
+ );
118
+ setSavedAssistants(assistants);
119
  } catch (error) {
120
+ console.error("Failed to load saved assistants:", error);
121
  }
122
+ };
123
 
124
  // Open save assistant dialog
125
  const openSaveDialog = () => {
126
+ if (!systemPrompt.trim()) return;
127
+
128
  // Set default name based on selected assistant or create a default
129
+ let defaultName = "My Assistant";
130
  if (selectedAssistant) {
131
+ if (selectedAssistant.type === "new") {
132
+ defaultName = selectedAssistant.name;
133
+ } else if (selectedAssistant.type === "template") {
134
+ defaultName = `My ${selectedAssistant.name}`;
135
  } else {
136
+ defaultName = selectedAssistant.name + " Copy";
137
  }
138
  }
139
+
140
+ setSaveAssistantName(defaultName);
141
+ setShowSaveDialog(true);
142
+ };
143
 
144
  // Actually save the assistant with user-defined name
145
  const confirmSaveAssistant = () => {
146
+ if (!saveAssistantName.trim() || !systemPrompt.trim()) return;
147
 
148
  const newAssistant = {
149
  id: Date.now().toString(),
 
155
  ragEnabled,
156
  retrievalCount,
157
  documents: [], // Assistant-specific documents (future: populated from DocumentsTab)
158
+ createdAt: new Date().toISOString(),
159
+ };
160
+
161
+ const updatedAssistants = [...savedAssistants, newAssistant];
162
+ setSavedAssistants(updatedAssistants);
163
+ localStorage.setItem("savedAssistants", JSON.stringify(updatedAssistants));
164
 
 
 
 
 
165
  // Update current selection to the newly saved assistant
166
  setSelectedAssistant({
167
  id: newAssistant.id,
168
  name: newAssistant.name,
169
+ type: "user",
170
+ });
171
+
172
  // Close dialog
173
+ setShowSaveDialog(false);
174
+ setSaveAssistantName("");
175
+ };
176
 
177
  // Cancel save dialog
178
  const cancelSaveDialog = () => {
179
+ setShowSaveDialog(false);
180
+ setSaveAssistantName("");
181
+ };
182
 
183
  // Load saved assistant
184
  const loadSavedAssistant = (assistantId: string) => {
185
+ const assistant = savedAssistants.find((a) => a.id === assistantId);
186
  if (assistant) {
187
+ setSystemPrompt(assistant.systemPrompt);
188
+ setTemperature(assistant.temperature);
189
+ setMaxTokens(assistant.maxTokens);
190
  // Load RAG settings with defaults for backwards compatibility
191
+ setRagEnabled(assistant.ragEnabled || false);
192
+ setRetrievalCount(assistant.retrievalCount || 3);
193
  if (assistant.model) {
194
+ setSelectedModel(assistant.model);
195
  }
196
  setSelectedAssistant({
197
  id: assistant.id,
198
  name: assistant.name,
199
+ type: "user",
200
+ });
201
  }
202
+ };
203
 
204
  // Preset system prompts from shared config
205
+ const systemPromptPresets = getPresetsFromConfigs();
206
 
207
  // Handle preset selection
208
  const handlePresetSelect = (presetName: string) => {
209
+ const preset = systemPromptPresets.find((p) => p.name === presetName);
210
  if (preset) {
211
+ setSystemPrompt(preset.prompt);
212
  // Reset RAG settings to defaults for templates
213
+ setRagEnabled(false);
214
+ setRetrievalCount(3);
215
  setSelectedAssistant({
216
  id: presetName,
217
  name: preset.name,
218
+ type: "template",
219
+ });
220
  }
221
+ };
222
 
223
  // Create new assistant (clear all settings but keep current session)
224
  const createNewAssistant = () => {
225
+ setSystemPrompt("");
226
+ setTemperature(0.7);
227
+ setMaxTokens(1024);
228
  // Reset RAG settings to defaults
229
+ setRagEnabled(false);
230
+ setRetrievalCount(3);
231
  setSelectedAssistant({
232
+ id: "new_assistant",
233
+ name: "New Assistant",
234
+ type: "new",
235
+ });
236
  // Keep current session - only clear assistant settings
237
+ };
238
 
239
  // Clear current assistant
240
  const clearCurrentAssistant = () => {
241
+ setSelectedAssistant(null);
242
+ };
243
 
244
  // Open rename assistant dialog
245
  const openRenameDialog = () => {
246
  if (selectedAssistant) {
247
+ setRenameAssistantName(selectedAssistant.name);
248
+ setShowRenameDialog(true);
249
  }
250
+ };
251
 
252
  // Confirm rename assistant
253
  const confirmRenameAssistant = () => {
254
+ if (!selectedAssistant || !renameAssistantName.trim()) return;
255
+
256
  // Update selectedAssistant state
257
  const updatedAssistant = {
258
  ...selectedAssistant,
259
+ name: renameAssistantName.trim(),
260
+ };
261
+ setSelectedAssistant(updatedAssistant);
262
+
263
  // If it's a saved user assistant, update localStorage
264
+ if (selectedAssistant.type === "user") {
265
+ const updatedAssistants = savedAssistants.map((assistant) =>
266
+ assistant.id === selectedAssistant.id
267
  ? { ...assistant, name: renameAssistantName.trim() }
268
  : assistant
269
+ );
270
+ setSavedAssistants(updatedAssistants);
271
+ localStorage.setItem(
272
+ "savedAssistants",
273
+ JSON.stringify(updatedAssistants)
274
+ );
275
  }
276
+
277
  // Close dialog
278
+ setShowRenameDialog(false);
279
+ setRenameAssistantName("");
280
+ };
281
 
282
  // Cancel rename dialog
283
  const cancelRenameDialog = () => {
284
+ setShowRenameDialog(false);
285
+ setRenameAssistantName("");
286
+ };
287
 
288
  // Get current assistant information
289
  const getCurrentAssistantInfo = (): AssistantInfo => {
290
  return {
291
+ name: selectedAssistant?.name || "Default Assistant",
292
+ type: selectedAssistant?.type || "default",
293
  systemPrompt,
294
  temperature,
295
  maxTokens,
296
+ originalTemplate: selectedAssistant?.originalTemplate,
297
+ };
298
+ };
299
 
300
  // Convert template to new assistant when settings change
301
  const convertTemplateToNew = () => {
302
+ if (selectedAssistant && selectedAssistant.type === "template") {
303
  setSelectedAssistant({
304
+ id: "new_assistant",
305
  name: `Custom ${selectedAssistant.name}`,
306
+ type: "new",
307
+ originalTemplate: selectedAssistant.name,
308
+ });
309
  }
310
+ };
311
 
312
  // Wrapped setters that trigger template conversion
313
  const handleSystemPromptChange = (prompt: string) => {
314
+ setSystemPrompt(prompt);
315
+ convertTemplateToNew();
316
+ };
317
 
318
  const handleTemperatureChange = (temp: number) => {
319
+ setTemperature(temp);
320
+ convertTemplateToNew();
321
+ };
322
 
323
  const handleMaxTokensChange = (tokens: number) => {
324
+ setMaxTokens(tokens);
325
+ convertTemplateToNew();
326
+ };
327
 
328
  const handleRagEnabledChange = (enabled: boolean) => {
329
+ setRagEnabled(enabled);
330
+ convertTemplateToNew();
331
+ };
332
 
333
  const handleRetrievalCountChange = (count: number) => {
334
+ setRetrievalCount(count);
335
+ convertTemplateToNew();
336
+ };
337
 
338
  // Load available models and saved assistants on startup
339
  useEffect(() => {
340
+ fetchModels();
341
+ loadSavedAssistants();
342
+
343
  // Check if there's a template configuration to load
344
+ const loadConfig = localStorage.getItem("loadAssistantConfig");
345
  if (loadConfig) {
346
  try {
347
+ const config = JSON.parse(loadConfig);
348
+ setSystemPrompt(config.systemPrompt || "");
349
+ setTemperature(config.temperature || 0.7);
350
+ setMaxTokens(config.maxTokens || 1024);
351
  if (config.model) {
352
+ setSelectedModel(config.model);
353
  }
354
  setSelectedAssistant({
355
+ id: "loaded_template",
356
+ name: config.name || "Loaded Template",
357
+ type: "template",
358
+ });
359
+
360
  // Clear the config after loading
361
+ localStorage.removeItem("loadAssistantConfig");
362
  } catch (error) {
363
+ console.error("Failed to load assistant config:", error);
364
+ localStorage.removeItem("loadAssistantConfig");
365
  }
366
  }
367
+ }, []);
368
 
369
  // Debug logs for Session issue
370
  useEffect(() => {
371
+ console.log("Sidebar states:", {
372
+ sessionsCollapsed,
373
+ configCollapsed,
374
+ sessionsCount: sessions.length,
375
+ currentSessionId,
376
+ });
377
+ }, [sessionsCollapsed, configCollapsed, sessions.length, currentSessionId]);
378
 
379
  useEffect(() => {
380
+ console.log(
381
+ "Rendering sessions:",
382
+ sessions.length,
383
+ sessions.map((s) => ({ id: s.id, title: s.title }))
384
+ );
385
+ }, [sessions]);
386
 
387
  // Update selected model when models change
388
  useEffect(() => {
389
  // Only reset if the selected model no longer exists in the models list
390
+ if (selectedModel && !models.find((m) => m.model_name === selectedModel)) {
391
+ const firstModel = models[0];
392
  if (firstModel) {
393
+ setSelectedModel(firstModel.model_name);
394
  }
395
  }
396
+ }, [models, selectedModel, setSelectedModel]);
397
 
398
  // Auto-load/unload local models when selection changes
399
  useEffect(() => {
400
  const handleModelChange = async () => {
401
+ if (!selectedModel || !models.length) return;
402
 
403
+ const selectedModelInfo = models.find(
404
+ (m) => m.model_name === selectedModel
405
+ );
406
+ if (!selectedModelInfo) return;
407
 
408
+ const baseUrl = `${window.location.protocol}//${window.location.host}`;
409
 
410
  // If selected model is a local model and not loaded, show confirmation
411
+ if (selectedModelInfo.type === "local" && !selectedModelInfo.is_loaded) {
412
+ setPendingModelToLoad(selectedModelInfo);
413
+ setShowLoadConfirm(true);
414
+ return; // Don't auto-load, wait for user confirmation
415
  }
416
 
417
  // Unload other local models that are loaded but not selected
418
+ const loadedLocalModels = models.filter(
419
+ (m) =>
420
+ m.type === "local" && m.is_loaded && m.model_name !== selectedModel
421
+ );
 
422
 
423
  for (const model of loadedLocalModels) {
424
  try {
425
  const response = await fetch(`${baseUrl}/unload-model`, {
426
+ method: "POST",
427
+ headers: { "Content-Type": "application/json" },
428
+ body: JSON.stringify({ model_name: model.model_name }),
429
+ });
430
+
431
  if (response.ok) {
432
+ console.log(`✅ Auto-unloaded local model: ${model.model_name}`);
433
  }
434
  } catch (error) {
435
+ console.error(
436
+ `Error auto-unloading model ${model.model_name}:`,
437
+ error
438
+ );
439
  }
440
  }
441
+
442
  // Refresh models after any unloading
443
  if (loadedLocalModels.length > 0) {
444
+ fetchModels();
445
  }
446
+ };
447
 
448
+ handleModelChange();
449
+ }, [selectedModel, models]);
450
 
451
  const handleLoadModelConfirm = async () => {
452
+ if (!pendingModelToLoad) return;
453
+
454
+ setShowLoadConfirm(false);
455
+ setAutoLoadingModel(pendingModelToLoad.model_name);
456
+
457
  try {
458
+ const baseUrl = `${window.location.protocol}//${window.location.host}`;
459
  const response = await fetch(`${baseUrl}/load-model`, {
460
+ method: "POST",
461
+ headers: { "Content-Type": "application/json" },
462
+ body: JSON.stringify({ model_name: pendingModelToLoad.model_name }),
463
+ });
464
+
465
  if (response.ok) {
466
+ console.log(
467
+ `✅ User confirmed and loaded: ${pendingModelToLoad.model_name}`
468
+ );
469
+ fetchModels(); // Refresh model states
470
  } else {
471
+ console.error(
472
+ `❌ Failed to load model: ${pendingModelToLoad.model_name}`
473
+ );
474
  // Revert to an API model if load failed
475
+ const apiModel = models.find((m) => m.type === "api");
476
  if (apiModel) {
477
+ setSelectedModel(apiModel.model_name);
478
  }
479
  }
480
  } catch (error) {
481
+ console.error("Error loading model:", error);
482
  // Revert to an API model if error
483
+ const apiModel = models.find((m) => m.type === "api");
484
  if (apiModel) {
485
+ setSelectedModel(apiModel.model_name);
486
  }
487
  } finally {
488
+ setAutoLoadingModel(null);
489
+ setPendingModelToLoad(null);
490
  }
491
+ };
492
 
493
  const handleLoadModelCancel = () => {
494
+ setShowLoadConfirm(false);
495
+ setPendingModelToLoad(null);
496
+
497
  // Revert to an API model
498
+ const apiModel = models.find((m) => m.type === "api");
499
  if (apiModel) {
500
+ setSelectedModel(apiModel.model_name);
501
  }
502
+ };
503
 
504
  // Cleanup: unload all local models when component unmounts or user leaves
505
  useEffect(() => {
506
  const handlePageUnload = async () => {
507
+ const baseUrl = `${window.location.protocol}//${window.location.host}`;
508
+ const loadedLocalModels = models.filter(
509
+ (m) => m.type === "local" && m.is_loaded
510
+ );
511
+
512
  for (const model of loadedLocalModels) {
513
  try {
514
  await fetch(`${baseUrl}/unload-model`, {
515
+ method: "POST",
516
+ headers: { "Content-Type": "application/json" },
517
+ body: JSON.stringify({ model_name: model.model_name }),
518
+ });
519
+ console.log(`✅ Cleanup: unloaded ${model.model_name}`);
520
  } catch (error) {
521
+ console.error(`Error cleaning up model ${model.model_name}:`, error);
522
  }
523
  }
524
+ };
525
 
526
  // Cleanup on component unmount
527
  return () => {
528
+ handlePageUnload();
529
+ };
530
+ }, [models]);
531
 
532
  const fetchModels = async () => {
533
  try {
534
+ const baseUrl = `${window.location.protocol}//${window.location.host}`;
535
+ const res = await fetch(`${baseUrl}/models`);
536
  if (res.ok) {
537
+ const data: ModelsResponse = await res.json();
538
+ setModels(data.models);
539
+
540
  // Set selected model to current model if available, otherwise first API model
541
  if (data.current_model && selectedModel !== data.current_model) {
542
+ setSelectedModel(data.current_model);
543
  } else if (!selectedModel && data.models.length > 0) {
544
  // Prefer API models as default
545
+ const apiModel = data.models.find((m) => m.type === "api");
546
+ const defaultModel = apiModel || data.models[0];
547
+ setSelectedModel(defaultModel.model_name);
548
  }
549
  }
550
  } catch (err) {
551
+ console.error("Failed to fetch models:", err);
552
  }
553
+ };
554
 
555
  return (
556
  <div className="h-screen bg-background flex">
557
  {/* Chat Sessions Sidebar */}
558
+ <div
559
+ className={`
560
  bg-background border-r flex-shrink-0 transition-all duration-300 ease-in-out
561
+ ${sessionsCollapsed ? "w-12" : "w-80"}
562
+ `}
563
+ >
564
  <div className="p-4 space-y-4 h-full">
565
+ <div
566
+ className={`flex items-center ${
567
+ sessionsCollapsed ? "justify-center" : "justify-between"
568
+ }`}
569
+ >
570
+ {!sessionsCollapsed && (
571
+ <h2 className="font-semibold">Chat Sessions</h2>
572
+ )}
573
  <div className="flex gap-1">
574
  {!sessionsCollapsed && (
575
  <Button onClick={clearCurrentSession} size="sm">
 
577
  New
578
  </Button>
579
  )}
580
+ <Button
581
+ onClick={() => setSessionsCollapsed(!sessionsCollapsed)}
582
+ size="sm"
583
  variant="ghost"
584
  className="h-8 w-8 p-0 hover:bg-gray-100"
585
+ title={
586
+ sessionsCollapsed ? "Expand Sessions" : "Collapse Sessions"
587
+ }
588
  >
589
+ {sessionsCollapsed ? (
590
+ <ChevronRight className="h-4 w-4" />
591
+ ) : (
592
+ <ChevronLeft className="h-4 w-4" />
593
+ )}
594
  </Button>
595
  </div>
596
  </div>
597
+
598
  {sessionsCollapsed && (
599
+ <Button
600
+ onClick={clearCurrentSession}
601
+ size="sm"
602
+ variant="ghost"
603
+ className="w-full p-2"
604
+ >
605
  <Plus className="h-4 w-4" />
606
  </Button>
607
  )}
608
  <div className="space-y-2">
609
+ {!sessionsCollapsed &&
610
+ sessions.map((session) => (
611
+ <Card
612
+ key={session.id}
613
+ className={`p-3 cursor-pointer transition-colors hover:bg-accent ${
614
+ currentSessionId === session.id
615
+ ? "bg-accent border-primary"
616
+ : ""
617
+ }`}
618
+ onClick={() => {
619
+ console.log(
620
+ "Session card clicked:",
621
+ session.id,
622
+ session.title
623
+ );
624
+ selectSession(session.id);
625
+ }}
626
+ >
627
+ <div className="flex items-center justify-between">
628
+ <span className="text-sm font-medium truncate">
629
+ {session.title}
630
+ </span>
631
+ <Button
632
+ size="sm"
633
+ variant="ghost"
634
+ onClick={(e) => {
635
+ e.stopPropagation();
636
+ deleteSession(session.id);
637
+ }}
638
+ className="h-6 w-6 p-0"
639
+ >
640
+ <Trash2 className="h-3 w-3" />
641
+ </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  </div>
643
+ <div className="text-xs text-muted-foreground">
644
+ {session.messages.length} messages
645
+ </div>
646
+ </Card>
647
+ ))}
648
+
649
+ {sessionsCollapsed &&
650
+ sessions.map((session) => (
651
+ <Button
652
+ key={session.id}
653
+ variant={
654
+ currentSessionId === session.id ? "default" : "ghost"
655
+ }
656
+ size="sm"
657
+ className="w-full p-1 h-8 relative"
658
+ title={session.title}
659
+ onClick={() => {
660
+ console.log(
661
+ "Session icon clicked:",
662
+ session.id,
663
+ session.title
664
+ );
665
+ selectSession(session.id);
666
+ }}
667
+ >
668
+ <MessageSquare className="h-4 w-4" />
669
+ {session.messages && session.messages.length > 0 && (
670
+ <div className="absolute -top-1 -right-1 w-3 h-3 bg-blue-500 text-white text-xs rounded-full flex items-center justify-center">
671
+ {Math.min(session.messages.length, 9)}
672
+ </div>
673
+ )}
674
+ </Button>
675
+ ))}
676
  </div>
677
  </div>
678
  </div>
 
685
  <div className="flex-1 flex flex-col">
686
  {/* Chat Messages and Input */}
687
  <Chat
688
+ messages={messages.map((msg) => ({
689
  id: msg.id,
690
+ role: msg.role as "user" | "assistant" | "system",
691
  content: msg.content,
692
  createdAt: new Date(msg.timestamp),
693
+ assistantInfo: msg.assistantInfo,
694
  }))}
695
  input={input}
696
  handleInputChange={(e) => setInput(e.target.value)}
697
  handleSubmit={async (e) => {
698
+ e.preventDefault();
699
+ if (
700
+ !selectedModel ||
701
+ !models.find((m) => m.model_name === selectedModel)
702
+ )
703
+ return;
704
+ const assistantInfo = getCurrentAssistantInfo();
705
+ const ragConfig = { useRag: ragEnabled, retrievalCount };
706
+ await sendMessage(assistantInfo, ragConfig);
707
  }}
708
  isGenerating={isLoading}
709
  stop={stopGeneration}
 
712
  </div>
713
 
714
  {/* Settings Panel - Tabbed Configuration Sidebar */}
715
+ <div
716
+ className={`
717
  border-l bg-background flex-shrink-0 transition-all duration-300 ease-in-out
718
+ ${configCollapsed ? "w-12" : "w-[480px] xl:w-[520px]"}
719
+ `}
720
+ >
721
  <div className="p-4 space-y-4 h-full">
722
+ <div
723
+ className={`flex items-center ${
724
+ configCollapsed ? "justify-center" : "justify-between"
725
+ }`}
726
+ >
727
+ {!configCollapsed && (
728
+ <h2 className="font-semibold">Configuration</h2>
729
+ )}
730
  <div className="flex gap-1">
731
+ <Button
732
+ onClick={() => setConfigCollapsed(!configCollapsed)}
733
+ size="sm"
734
  variant="ghost"
735
  className="h-8 w-8 p-0 hover:bg-gray-100"
736
+ title={
737
+ configCollapsed
738
+ ? "Expand Configuration"
739
+ : "Collapse Configuration"
740
+ }
741
  >
742
+ {configCollapsed ? (
743
+ <ChevronLeft className="h-4 w-4" />
744
+ ) : (
745
+ <ChevronRight className="h-4 w-4" />
746
+ )}
747
  </Button>
748
  </div>
749
  </div>
750
+
751
  {configCollapsed && (
752
+ <Button
753
+ size="sm"
754
+ variant="ghost"
755
+ className="w-full p-2 invisible"
756
+ >
757
  <Settings className="h-4 w-4" />
758
  </Button>
759
  )}
 
762
  <>
763
  {/* Assistant Selection Section */}
764
  <div className="border-b bg-white p-4 -mx-4">
765
+ <AssistantSelector
766
  savedAssistants={savedAssistants}
767
  loadSavedAssistant={loadSavedAssistant}
768
  openSaveDialog={openSaveDialog}
 
777
  />
778
  </div>
779
 
780
+ <Tabs
781
+ defaultValue="parameters"
782
+ className="flex-1 flex flex-col -mx-4"
783
+ >
784
+ <TabsList className="grid w-full grid-cols-3 m-4 mb-0">
785
+ <TabsTrigger
786
+ value="parameters"
787
+ className="flex items-center gap-2"
788
+ >
789
+ <Sliders className="h-4 w-4" />
790
+ <span className="hidden sm:inline">Parameters</span>
791
+ </TabsTrigger>
792
+ <TabsTrigger
793
+ value="instructions"
794
+ className="flex items-center gap-2"
795
+ >
796
+ <Settings className="h-4 w-4" />
797
+ <span className="hidden sm:inline">Instructions</span>
798
+ </TabsTrigger>
799
+ <TabsTrigger
800
+ value="documents"
801
+ className="flex items-center gap-2"
802
+ >
803
+ <BookOpen className="h-4 w-4" />
804
+ <span className="hidden sm:inline">Documents</span>
805
+ </TabsTrigger>
806
+ </TabsList>
807
+
808
+ <div className="flex-1 overflow-hidden">
809
+ <TabsContent
810
+ value="parameters"
811
+ className="p-6 space-y-6 m-0 h-full overflow-y-auto"
812
+ >
813
+ <ModelParametersTab
814
+ models={models}
815
+ selectedModel={selectedModel}
816
+ setSelectedModel={setSelectedModel}
817
+ autoLoadingModel={autoLoadingModel}
818
+ temperature={temperature}
819
+ setTemperature={handleTemperatureChange}
820
+ maxTokens={maxTokens}
821
+ setMaxTokens={handleMaxTokensChange}
822
+ />
823
+ </TabsContent>
824
+
825
+ <TabsContent
826
+ value="instructions"
827
+ className="p-6 space-y-6 m-0 h-full overflow-y-auto"
828
+ >
829
+ <SystemInstructionsTab
830
+ systemPrompt={systemPrompt}
831
+ setSystemPrompt={handleSystemPromptChange}
832
+ isLoading={isLoading}
833
+ />
834
+ </TabsContent>
835
+
836
+ <TabsContent
837
+ value="documents"
838
+ className="p-6 space-y-6 m-0 h-full overflow-y-auto"
839
+ >
840
+ <DocumentsTab
841
+ isLoading={isLoading}
842
+ ragEnabled={ragEnabled}
843
+ setRagEnabled={handleRagEnabledChange}
844
+ retrievalCount={retrievalCount}
845
+ setRetrievalCount={handleRetrievalCountChange}
846
+ currentAssistant={selectedAssistant}
847
+ />
848
+ </TabsContent>
849
+ </div>
850
+ </Tabs>
851
  </>
852
  )}
853
  </div>
 
862
  <AlertDialogHeader>
863
  <AlertDialogTitle>Load Local Model</AlertDialogTitle>
864
  <AlertDialogDescription>
865
+ Loading <strong>{pendingModelToLoad?.name}</strong> will use
866
+ approximately <strong>{pendingModelToLoad?.size_gb}</strong> of
867
+ RAM and storage. First-time loading may require downloading the
868
+ model.
869
  </AlertDialogDescription>
870
  </AlertDialogHeader>
871
  <AlertDialogFooter>
 
885
  <AlertDialogHeader>
886
  <AlertDialogTitle>Save Assistant</AlertDialogTitle>
887
  <AlertDialogDescription>
888
+ Give your assistant a custom name. This will be displayed in your
889
+ assistant list.
890
  </AlertDialogDescription>
891
  </AlertDialogHeader>
892
  <div className="py-4">
 
903
  maxLength={100}
904
  autoFocus
905
  onKeyDown={(e) => {
906
+ if (e.key === "Enter" && saveAssistantName.trim()) {
907
+ confirmSaveAssistant();
908
  }
909
+ if (e.key === "Escape") {
910
+ cancelSaveDialog();
911
  }
912
  }}
913
  />
 
919
  <AlertDialogCancel onClick={cancelSaveDialog}>
920
  Cancel
921
  </AlertDialogCancel>
922
+ <AlertDialogAction
923
  onClick={confirmSaveAssistant}
924
  disabled={!saveAssistantName.trim()}
925
  >
 
940
  </AlertDialogDescription>
941
  </AlertDialogHeader>
942
  <div className="py-4">
943
+ <Label
944
+ htmlFor="rename-assistant-name"
945
+ className="text-sm font-medium"
946
+ >
947
  Assistant Name
948
  </Label>
949
  <input
 
956
  maxLength={100}
957
  autoFocus
958
  onKeyDown={(e) => {
959
+ if (e.key === "Enter" && renameAssistantName.trim()) {
960
+ confirmRenameAssistant();
961
  }
962
+ if (e.key === "Escape") {
963
+ cancelRenameDialog();
964
  }
965
  }}
966
  />
 
972
  <AlertDialogCancel onClick={cancelRenameDialog}>
973
  Cancel
974
  </AlertDialogCancel>
975
+ <AlertDialogAction
976
  onClick={confirmRenameAssistant}
977
  disabled={!renameAssistantName.trim()}
978
  >
 
983
  </AlertDialogContent>
984
  </AlertDialog>
985
  </div>
986
+ );
987
  }
frontend/src/pages/Technology.tsx CHANGED
@@ -1,13 +1,13 @@
1
- import { useState } from 'react'
2
- import { useNavigate } from 'react-router-dom'
3
- import { Button } from '@/components/ui/button'
4
- import { Badge } from '@/components/ui/badge'
5
- import { Card, CardContent } from '@/components/ui/card'
6
- import { Menu } from 'lucide-react'
7
 
8
  export function Technology() {
9
- const [activeTab, setActiveTab] = useState('Hardware')
10
- const navigate = useNavigate()
11
 
12
  return (
13
  <div className="min-h-screen bg-gray-50">
@@ -17,34 +17,37 @@ export function Technology() {
17
  <div className="flex justify-between items-center h-20">
18
  {/* Logo */}
19
  <div className="flex items-center">
20
- <img
21
- src="/assets/logo.png"
22
- alt="EdgeLLM Logo"
23
  className="h-20 w-20"
24
  onError={(e) => {
25
- console.error('Logo failed to load')
26
- e.currentTarget.style.display = 'none'
27
  }}
28
  />
29
  </div>
30
 
31
  {/* Navigation Links */}
32
  <div className="hidden md:flex items-center space-x-8">
33
- <button onClick={() => navigate('/')} className="text-gray-700 hover:text-purple-600 font-medium">
 
 
 
34
  Home
35
  </button>
36
  <span className="text-white bg-purple-600 px-4 py-2 rounded-md font-medium">
37
  Technology
38
  </span>
39
- <button
40
- onClick={() => navigate('/usecases')}
41
  className="text-gray-700 hover:text-purple-600 font-medium transition-colors"
42
  >
43
  Use Cases
44
  </button>
45
- <Button
46
- variant="outline"
47
- onClick={() => navigate('/mydevice')}
48
  className="border-purple-600 text-purple-600 hover:bg-purple-50"
49
  >
50
  My Device
@@ -61,28 +64,29 @@ export function Technology() {
61
  </div>
62
  </nav>
63
 
64
- {/* Header Section */}
65
- <div className="py-24 px-4 sm:px-6 lg:px-8">
66
- <div className="max-w-4xl mx-auto text-center">
67
- {/* Title */}
68
- <h1 className="text-4xl md:text-5xl font-bold text-black mb-6">
69
- Our Technology
70
- </h1>
71
  <p className="text-xl text-purple-700 font-semibold max-w-2xl mx-auto mb-8">
72
- Low-cost FPGA-based design, optimized for Large Language Models on-device.
 
73
  </p>
74
 
75
  {/* Tabs */}
76
  <div className="flex justify-center mb-12">
77
  <div className="bg-white rounded-lg p-1 shadow-sm">
78
- {['Hardware', 'Software'].map((tab) => (
79
  <button
80
  key={tab}
81
  onClick={() => setActiveTab(tab)}
82
  className={`px-6 py-2 rounded-md text-sm font-medium transition-all ${
83
  activeTab === tab
84
- ? 'bg-purple-600 text-white'
85
- : 'text-gray-700 hover:text-purple-600'
86
  }`}
87
  >
88
  {tab}
@@ -95,7 +99,7 @@ export function Technology() {
95
 
96
  {/* Tab Content */}
97
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
98
- {activeTab === 'Hardware' && (
99
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
100
  {/* Hardware Text Content */}
101
  <div>
@@ -103,21 +107,26 @@ export function Technology() {
103
  Hardware Description
104
  </h2>
105
  <p className="text-lg text-gray-800 leading-relaxed">
106
- EdgeMate is an ultra–low-budget single-board computer designed specifically for large language model (LLM) deployment.
107
- Despite its compact form factor and low power consumption, we provide:
 
108
  </p>
109
  <ul className="mt-6 space-y-3 text-lg text-gray-800">
110
  <li className="flex items-start">
111
  <span className="text-purple-600 mr-2">•</span>
112
- Accelerated inference performance — delivering up to 15 tokens per second on a 30B model, thanks to its optimized FPGA-based AI engine.
 
 
113
  </li>
114
  <li className="flex items-start">
115
  <span className="text-purple-600 mr-2">•</span>
116
- High memory capacity — up to 40GB RAM, enabling hosting of LLMs up to ≥30B parameters.
 
117
  </li>
118
  <li className="flex items-start">
119
  <span className="text-purple-600 mr-2">•</span>
120
- Cost efficiency — making advanced AI workloads accessible at low cost.
 
121
  </li>
122
  </ul>
123
  </div>
@@ -127,12 +136,18 @@ export function Technology() {
127
  <CardContent className="p-8 space-y-6">
128
  <div className="flex flex-col space-y-4">
129
  <div className="flex justify-between items-start">
130
- <span className="text-lg font-bold text-gray-900">Chip</span>
131
- <span className="text-base text-gray-700">AMD Zynq UltraScale+ XCZU3EG</span>
 
 
 
 
132
  </div>
133
-
134
  <div className="flex justify-between items-start">
135
- <span className="text-lg font-bold text-gray-900">Processor</span>
 
 
136
  <div className="text-base text-gray-700 text-right">
137
  <div>Quad-core 64-bit Arm Cortex-A53 CPU</div>
138
  <div>Dual-core 64-bit Arm Cortex-R5 CPU</div>
@@ -141,20 +156,31 @@ export function Technology() {
141
  </div>
142
 
143
  <div className="flex justify-between items-start">
144
- <span className="text-lg font-bold text-gray-900">FPGA Fabric</span>
145
- <span className="text-base text-gray-700">70K LUT, 360 DSP slices</span>
 
 
 
 
146
  </div>
147
 
148
  <div className="flex justify-between items-start">
149
- <span className="text-lg font-bold text-gray-900">Memory</span>
 
 
150
  <div className="text-base text-gray-700 text-right">
151
  <div>8 GB 64-bit DDR4 (2400 Mbps) on CPU side</div>
152
- <div>8 GB / 16 GB / 32 GB DDR4 (2133 Mbps) on FPGA side (SODIMM)</div>
 
 
 
153
  </div>
154
  </div>
155
 
156
  <div className="flex justify-between items-start">
157
- <span className="text-lg font-bold text-gray-900">Storage</span>
 
 
158
  <div className="text-base text-gray-700 text-right">
159
  <div>256 GB PCIe 2.0 x1 NVMe SSD</div>
160
  <div>MicroSD card slot</div>
@@ -174,7 +200,7 @@ export function Technology() {
174
  </div>
175
  )}
176
 
177
- {activeTab === 'Software' && (
178
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
179
  {/* Software Text Content */}
180
  <div>
@@ -182,25 +208,30 @@ export function Technology() {
182
  Software Stack
183
  </h2>
184
  <p className="text-lg text-gray-800 leading-relaxed">
185
- Our comprehensive software ecosystem is designed to maximize the potential of EdgeMate hardware,
186
- providing seamless LLM deployment and management capabilities:
 
187
  </p>
188
  <ul className="mt-6 space-y-3 text-lg text-gray-800">
189
  <li className="flex items-start">
190
  <span className="text-purple-600 mr-2">•</span>
191
- Custom FPGA-optimized inference engine with quantization support for efficient model deployment.
 
192
  </li>
193
  <li className="flex items-start">
194
  <span className="text-purple-600 mr-2">•</span>
195
- Edge LLM Platform intuitive web interface for model management, chat, and system monitoring.
 
196
  </li>
197
  <li className="flex items-start">
198
  <span className="text-purple-600 mr-2">•</span>
199
- Support for popular model formats (GGUF, ONNX) and frameworks (Transformers, LLaMA.cpp).
 
200
  </li>
201
  <li className="flex items-start">
202
  <span className="text-purple-600 mr-2">•</span>
203
- REST API for seamless integration with existing applications and services.
 
204
  </li>
205
  </ul>
206
  </div>
@@ -210,43 +241,53 @@ export function Technology() {
210
  <CardContent className="p-8 space-y-6">
211
  <div className="flex flex-col space-y-4">
212
  <div className="flex justify-between items-start">
213
- <span className="text-lg font-semibold text-gray-900">OS Support</span>
 
 
214
  <div className="text-base text-gray-700 text-right">
215
- <div>Ubuntu 22.04 LTS</div>
216
- <div>Custom Linux Distribution</div>
 
217
  </div>
218
  </div>
219
-
220
  <div className="flex justify-between items-start">
221
- <span className="text-lg font-semibold text-gray-900">Model Formats</span>
 
 
222
  <div className="text-base text-gray-700 text-right">
223
- <div>GGUF (LLaMA.cpp)</div>
224
- <div>ONNX</div>
225
- <div>PyTorch</div>
226
- <div>Transformers</div>
227
  </div>
228
  </div>
229
 
230
  <div className="flex justify-between items-start">
231
- <span className="text-lg font-semibold text-gray-900">Inference Engine</span>
 
 
232
  <div className="text-base text-gray-700 text-right">
233
- <div>Custom FPGA Accelerator</div>
234
- <div>LLaMA.cpp Integration</div>
235
  <div>Quantization Support</div>
236
  </div>
237
  </div>
238
 
239
  <div className="flex justify-between items-start">
240
- <span className="text-lg font-semibold text-gray-900">Management Interface</span>
 
 
241
  <div className="text-base text-gray-700 text-right">
242
- <div>Web-based Dashboard</div>
243
- <div>REST API</div>
244
- <div>CLI Tools</div>
245
  </div>
246
  </div>
247
 
248
  <div className="flex justify-between items-start">
249
- <span className="text-lg font-semibold text-gray-900">Development Tools</span>
 
 
250
  <div className="text-base text-gray-700 text-right">
251
  <div>Python SDK</div>
252
  <div>Model Optimization Tools</div>
@@ -255,7 +296,9 @@ export function Technology() {
255
  </div>
256
 
257
  <div className="flex justify-between items-start">
258
- <span className="text-lg font-semibold text-gray-900">Security</span>
 
 
259
  <div className="text-base text-gray-700 text-right">
260
  <div>Secure Boot</div>
261
  <div>Model Encryption</div>
@@ -272,14 +315,16 @@ export function Technology() {
272
  {/* Why us? Comparison Section */}
273
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
274
  <h2 className="text-3xl font-semibold text-purple-600 mb-8">Why us?</h2>
275
-
276
  {/* Comparison Table */}
277
  <div className="bg-white rounded-3xl shadow-xl overflow-hidden">
278
  {/* Table Header */}
279
  <div className="grid grid-cols-4 bg-gradient-to-r from-gray-50 to-gray-100">
280
  <div className="p-6"></div>
281
  <div className="p-6 text-center">
282
- <Badge className="bg-purple-600 text-white font-bold px-4 py-2">Ours</Badge>
 
 
283
  </div>
284
  <div className="p-6 text-center">
285
  <span className="font-bold text-gray-800">Raspberry Pi 5</span>
@@ -291,20 +336,40 @@ export function Technology() {
291
 
292
  {/* Table Rows */}
293
  {[
294
- { label: 'Price', ours: '$199', pi: '$120', jetson: '$249' },
295
- { label: 'RAM', ours: '24GB/40GB', pi: '16GB', jetson: '8GB' },
296
- { label: 'CPU', ours: 'Cortex-A53', pi: 'Cortex-A72', jetson: 'Cortex-A78' },
297
- { label: 'AI Engine', ours: 'Optimized Accelerator on FPGA', pi: 'Neon SIMD Instructions', jetson: 'Cuda/Tensor Core' },
298
- { label: 'Power', ours: '<10W', pi: '5-12 W', jetson: '7-25' },
299
- { label: 'LLM decode Performance', ours: '15 tokens/s', pi: '<5 tokens/s', jetson: '15 tokens/s' },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  ].map((row, index) => (
301
- <div key={row.label} className={`grid grid-cols-4 ${index % 2 === 0 ? 'bg-gray-50' : 'bg-white'}`}>
 
 
 
 
 
302
  <div className="p-4 font-medium text-gray-900 border-r border-gray-200">
303
  {row.label}
304
  </div>
305
- <div
306
- className="p-4 text-center font-medium"
307
- style={{ backgroundColor: '#F0EEF5', color: '#6750A4' }}
308
  >
309
  {row.ours}
310
  </div>
@@ -319,8 +384,8 @@ export function Technology() {
319
  </div>
320
 
321
  <div className="text-center mt-8">
322
- <Button
323
- onClick={() => navigate('/usecases')}
324
  className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-3"
325
  >
326
  View use cases
@@ -330,26 +395,31 @@ export function Technology() {
330
 
331
  {/* Device Layout Section */}
332
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
333
- <h2 className="text-3xl font-semibold text-purple-600 mb-8">The device layout plan</h2>
334
-
 
 
335
  <div className="bg-white rounded-2xl shadow-xl p-8 text-center">
336
  <div className="relative max-w-4xl mx-auto">
337
- <img
338
- src="/assets/chips.png"
339
- alt="Device Layout Plan"
340
- className="w-full h-auto rounded-lg shadow-lg"
341
- onError={(e) => {
342
- console.error('Chips image failed to load')
343
- e.currentTarget.style.display = 'none'
344
- }}
345
- />
346
  </div>
347
-
348
  <div className="flex justify-center space-x-4 mt-8">
349
  <Button className="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2">
350
  Get your device
351
  </Button>
352
- <Button variant="outline" className="border-purple-600 text-purple-600 hover:bg-purple-50 px-6 py-2">
 
 
 
353
  View use cases
354
  </Button>
355
  </div>
@@ -360,5 +430,5 @@ export function Technology() {
360
  </p>
361
  </div>
362
  </div>
363
- )
364
  }
 
1
+ import { useState } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Card, CardContent } from "@/components/ui/card";
6
+ import { Menu } from "lucide-react";
7
 
8
  export function Technology() {
9
+ const [activeTab, setActiveTab] = useState("Hardware");
10
+ const navigate = useNavigate();
11
 
12
  return (
13
  <div className="min-h-screen bg-gray-50">
 
17
  <div className="flex justify-between items-center h-20">
18
  {/* Logo */}
19
  <div className="flex items-center">
20
+ <img
21
+ src="/assets/logo.png"
22
+ alt="EdgeLLM Logo"
23
  className="h-20 w-20"
24
  onError={(e) => {
25
+ console.error("Logo failed to load");
26
+ e.currentTarget.style.display = "none";
27
  }}
28
  />
29
  </div>
30
 
31
  {/* Navigation Links */}
32
  <div className="hidden md:flex items-center space-x-8">
33
+ <button
34
+ onClick={() => navigate("/")}
35
+ className="text-gray-700 hover:text-purple-600 font-medium"
36
+ >
37
  Home
38
  </button>
39
  <span className="text-white bg-purple-600 px-4 py-2 rounded-md font-medium">
40
  Technology
41
  </span>
42
+ <button
43
+ onClick={() => navigate("/usecases")}
44
  className="text-gray-700 hover:text-purple-600 font-medium transition-colors"
45
  >
46
  Use Cases
47
  </button>
48
+ <Button
49
+ variant="outline"
50
+ onClick={() => navigate("/mydevice")}
51
  className="border-purple-600 text-purple-600 hover:bg-purple-50"
52
  >
53
  My Device
 
64
  </div>
65
  </nav>
66
 
67
+ {/* Header Section */}
68
+ <div className="py-24 px-4 sm:px-6 lg:px-8">
69
+ <div className="max-w-4xl mx-auto text-center">
70
+ {/* Title */}
71
+ <h1 className="text-4xl md:text-5xl font-bold text-black mb-6">
72
+ Our Technology
73
+ </h1>
74
  <p className="text-xl text-purple-700 font-semibold max-w-2xl mx-auto mb-8">
75
+ Low-cost FPGA-based design, optimized for Large Language Models
76
+ on-device.
77
  </p>
78
 
79
  {/* Tabs */}
80
  <div className="flex justify-center mb-12">
81
  <div className="bg-white rounded-lg p-1 shadow-sm">
82
+ {["Hardware", "Software"].map((tab) => (
83
  <button
84
  key={tab}
85
  onClick={() => setActiveTab(tab)}
86
  className={`px-6 py-2 rounded-md text-sm font-medium transition-all ${
87
  activeTab === tab
88
+ ? "bg-purple-600 text-white"
89
+ : "text-gray-700 hover:text-purple-600"
90
  }`}
91
  >
92
  {tab}
 
99
 
100
  {/* Tab Content */}
101
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
102
+ {activeTab === "Hardware" && (
103
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
104
  {/* Hardware Text Content */}
105
  <div>
 
107
  Hardware Description
108
  </h2>
109
  <p className="text-lg text-gray-800 leading-relaxed">
110
+ EdgeMate is an ultra–low-budget single-board computer designed
111
+ specifically for large language model (LLM) deployment. Despite
112
+ its compact form factor and low power consumption, we provide:
113
  </p>
114
  <ul className="mt-6 space-y-3 text-lg text-gray-800">
115
  <li className="flex items-start">
116
  <span className="text-purple-600 mr-2">•</span>
117
+ Accelerated inference performance — delivering up to 15 tokens
118
+ per second on a 30B model, thanks to its optimized FPGA-based
119
+ AI engine.
120
  </li>
121
  <li className="flex items-start">
122
  <span className="text-purple-600 mr-2">•</span>
123
+ High memory capacity — up to 40GB RAM, enabling hosting of
124
+ LLMs up to ≥30B parameters.
125
  </li>
126
  <li className="flex items-start">
127
  <span className="text-purple-600 mr-2">•</span>
128
+ Cost efficiency — making advanced AI workloads accessible at
129
+ low cost.
130
  </li>
131
  </ul>
132
  </div>
 
136
  <CardContent className="p-8 space-y-6">
137
  <div className="flex flex-col space-y-4">
138
  <div className="flex justify-between items-start">
139
+ <span className="text-lg font-bold text-gray-900">
140
+ Chip
141
+ </span>
142
+ <span className="text-base text-gray-700">
143
+ AMD Zynq UltraScale+ XCZU3EG
144
+ </span>
145
  </div>
146
+
147
  <div className="flex justify-between items-start">
148
+ <span className="text-lg font-bold text-gray-900">
149
+ Processor
150
+ </span>
151
  <div className="text-base text-gray-700 text-right">
152
  <div>Quad-core 64-bit Arm Cortex-A53 CPU</div>
153
  <div>Dual-core 64-bit Arm Cortex-R5 CPU</div>
 
156
  </div>
157
 
158
  <div className="flex justify-between items-start">
159
+ <span className="text-lg font-bold text-gray-900">
160
+ FPGA Fabric
161
+ </span>
162
+ <span className="text-base text-gray-700">
163
+ 70K LUT, 360 DSP slices
164
+ </span>
165
  </div>
166
 
167
  <div className="flex justify-between items-start">
168
+ <span className="text-lg font-bold text-gray-900">
169
+ Memory
170
+ </span>
171
  <div className="text-base text-gray-700 text-right">
172
  <div>8 GB 64-bit DDR4 (2400 Mbps) on CPU side</div>
173
+ <div>
174
+ 8 GB / 16 GB / 32 GB DDR4 (2133 Mbps) on FPGA side
175
+ (SODIMM)
176
+ </div>
177
  </div>
178
  </div>
179
 
180
  <div className="flex justify-between items-start">
181
+ <span className="text-lg font-bold text-gray-900">
182
+ Storage
183
+ </span>
184
  <div className="text-base text-gray-700 text-right">
185
  <div>256 GB PCIe 2.0 x1 NVMe SSD</div>
186
  <div>MicroSD card slot</div>
 
200
  </div>
201
  )}
202
 
203
+ {activeTab === "Software" && (
204
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
205
  {/* Software Text Content */}
206
  <div>
 
208
  Software Stack
209
  </h2>
210
  <p className="text-lg text-gray-800 leading-relaxed">
211
+ Our comprehensive software platform enables non-experts to
212
+ personalize, test, and deploy LLM agents to edge devices with an
213
+ intuitive web-based interface:
214
  </p>
215
  <ul className="mt-6 space-y-3 text-lg text-gray-800">
216
  <li className="flex items-start">
217
  <span className="text-purple-600 mr-2">•</span>
218
+ React 18 + TypeScript frontend with shadcn/ui design system
219
+ for modern, accessible components.
220
  </li>
221
  <li className="flex items-start">
222
  <span className="text-purple-600 mr-2">•</span>
223
+ FastAPI + Python 3.11+ backend with hybrid inference routing
224
+ between local FPGA and cloud APIs.
225
  </li>
226
  <li className="flex items-start">
227
  <span className="text-purple-600 mr-2">•</span>
228
+ Integrated RAG system using LangChain, FAISS, and HuggingFace
229
+ Transformers for document processing.
230
  </li>
231
  <li className="flex items-start">
232
  <span className="text-purple-600 mr-2">•</span>
233
+ Docker containerization with deployment on Hugging Face Spaces
234
+ for accessible demonstrations.
235
  </li>
236
  </ul>
237
  </div>
 
241
  <CardContent className="p-8 space-y-6">
242
  <div className="flex flex-col space-y-4">
243
  <div className="flex justify-between items-start">
244
+ <span className="text-lg font-semibold text-gray-900">
245
+ Frontend Stack
246
+ </span>
247
  <div className="text-base text-gray-700 text-right">
248
+ <div>React 18 + TypeScript</div>
249
+ <div>shadcn/ui + Radix UI</div>
250
+ <div>Tailwind CSS + Vite</div>
251
  </div>
252
  </div>
253
+
254
  <div className="flex justify-between items-start">
255
+ <span className="text-lg font-semibold text-gray-900">
256
+ Backend Stack
257
+ </span>
258
  <div className="text-base text-gray-700 text-right">
259
+ <div>FastAPI + Python 3.11+</div>
260
+ <div>HuggingFace Transformers</div>
261
+ <div>OpenAI-compatible APIs</div>
 
262
  </div>
263
  </div>
264
 
265
  <div className="flex justify-between items-start">
266
+ <span className="text-lg font-semibold text-gray-900">
267
+ AI/ML Components
268
+ </span>
269
  <div className="text-base text-gray-700 text-right">
270
+ <div>LangChain + FAISS</div>
271
+ <div>sentence-transformers</div>
272
  <div>Quantization Support</div>
273
  </div>
274
  </div>
275
 
276
  <div className="flex justify-between items-start">
277
+ <span className="text-lg font-semibold text-gray-900">
278
+ Deployment
279
+ </span>
280
  <div className="text-base text-gray-700 text-right">
281
+ <div>Docker Containerization</div>
282
+ <div>Hugging Face Spaces</div>
283
+ <div>REST API Integration</div>
284
  </div>
285
  </div>
286
 
287
  <div className="flex justify-between items-start">
288
+ <span className="text-lg font-semibold text-gray-900">
289
+ Development Tools
290
+ </span>
291
  <div className="text-base text-gray-700 text-right">
292
  <div>Python SDK</div>
293
  <div>Model Optimization Tools</div>
 
296
  </div>
297
 
298
  <div className="flex justify-between items-start">
299
+ <span className="text-lg font-semibold text-gray-900">
300
+ Security
301
+ </span>
302
  <div className="text-base text-gray-700 text-right">
303
  <div>Secure Boot</div>
304
  <div>Model Encryption</div>
 
315
  {/* Why us? Comparison Section */}
316
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
317
  <h2 className="text-3xl font-semibold text-purple-600 mb-8">Why us?</h2>
318
+
319
  {/* Comparison Table */}
320
  <div className="bg-white rounded-3xl shadow-xl overflow-hidden">
321
  {/* Table Header */}
322
  <div className="grid grid-cols-4 bg-gradient-to-r from-gray-50 to-gray-100">
323
  <div className="p-6"></div>
324
  <div className="p-6 text-center">
325
+ <Badge className="bg-purple-600 text-white font-bold px-4 py-2">
326
+ Ours
327
+ </Badge>
328
  </div>
329
  <div className="p-6 text-center">
330
  <span className="font-bold text-gray-800">Raspberry Pi 5</span>
 
336
 
337
  {/* Table Rows */}
338
  {[
339
+ { label: "Price", ours: "$199", pi: "$120", jetson: "$249" },
340
+ { label: "RAM", ours: "24GB/40GB", pi: "16GB", jetson: "8GB" },
341
+ {
342
+ label: "CPU",
343
+ ours: "Cortex-A53",
344
+ pi: "Cortex-A72",
345
+ jetson: "Cortex-A78",
346
+ },
347
+ {
348
+ label: "AI Engine",
349
+ ours: "Optimized Accelerator on FPGA",
350
+ pi: "Neon SIMD Instructions",
351
+ jetson: "Cuda/Tensor Core",
352
+ },
353
+ { label: "Power", ours: "<10W", pi: "5-12 W", jetson: "7-25" },
354
+ {
355
+ label: "LLM decode Performance",
356
+ ours: "15 tokens/s",
357
+ pi: "<5 tokens/s",
358
+ jetson: "15 tokens/s",
359
+ },
360
  ].map((row, index) => (
361
+ <div
362
+ key={row.label}
363
+ className={`grid grid-cols-4 ${
364
+ index % 2 === 0 ? "bg-gray-50" : "bg-white"
365
+ }`}
366
+ >
367
  <div className="p-4 font-medium text-gray-900 border-r border-gray-200">
368
  {row.label}
369
  </div>
370
+ <div
371
+ className="p-4 text-center font-medium"
372
+ style={{ backgroundColor: "#F0EEF5", color: "#6750A4" }}
373
  >
374
  {row.ours}
375
  </div>
 
384
  </div>
385
 
386
  <div className="text-center mt-8">
387
+ <Button
388
+ onClick={() => navigate("/usecases")}
389
  className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-3"
390
  >
391
  View use cases
 
395
 
396
  {/* Device Layout Section */}
397
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
398
+ <h2 className="text-3xl font-semibold text-purple-600 mb-8">
399
+ The device layout plan
400
+ </h2>
401
+
402
  <div className="bg-white rounded-2xl shadow-xl p-8 text-center">
403
  <div className="relative max-w-4xl mx-auto">
404
+ <img
405
+ src="/assets/chips.png"
406
+ alt="Device Layout Plan"
407
+ className="w-full h-auto rounded-lg shadow-lg"
408
+ onError={(e) => {
409
+ console.error("Chips image failed to load");
410
+ e.currentTarget.style.display = "none";
411
+ }}
412
+ />
413
  </div>
414
+
415
  <div className="flex justify-center space-x-4 mt-8">
416
  <Button className="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2">
417
  Get your device
418
  </Button>
419
+ <Button
420
+ variant="outline"
421
+ className="border-purple-600 text-purple-600 hover:bg-purple-50 px-6 py-2"
422
+ >
423
  View use cases
424
  </Button>
425
  </div>
 
430
  </p>
431
  </div>
432
  </div>
433
+ );
434
  }
software_implementation.tex ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ \section{Demo Overview}
2
+
3
+ We demonstrate a complete web-based platform that enables non-experts to personalize, test, and deploy LLM agents to edge devices. Our live demo showcases three key capabilities: (1) \textit{Interactive Model Tuning} - users adjust parameters and prompts in real-time using intuitive sliders and text editors, (2) \textit{Scenario-Based Agent Creation} - pre-built templates transform base models into specialized assistants (outdoor rescue, healthcare companion, AI tutor), and (3) \textit{Seamless Edge Deployment} - one-click transfer of configured agents to FPGA hardware for offline operation.
4
+
5
+ The system architecture combines a React frontend with FastAPI backend, featuring hybrid inference that intelligently routes between local FPGA acceleration and cloud APIs.
6
+
7
+ \textbf{Frontend Technology Stack:} Built with React 18 and TypeScript for type-safe development, using Vite as the modern build system for fast development and optimized production builds. The UI implementation follows shadcn/ui design system, combining Radix UI primitives (alert-dialog, select, slider, tabs) with Tailwind CSS 3.3.0 for accessible, consistent components. The component architecture uses class-variance-authority for variant management, clsx and tailwind-merge for style composition, and Lucide React for iconography. State management uses React hooks with localStorage persistence via a custom ChatStorageManager. The chat interface integrates Vercel AI SDK for streaming responses, with react-markdown for rendering formatted content. Navigation is handled by React Router DOM 6.15.0 with nested routing structure.
8
+
9
+ \textbf{Backend Technology Stack:} Implemented with FastAPI and Python 3.11+, providing async request handling and automatic OpenAPI documentation. Model inference combines HuggingFace Transformers for local models with OpenAI-compatible API clients for cloud models. The RAG system uses LangChain for document processing (PDF, DOCX, TXT, MD via pypdf, python-docx, unstructured), FAISS for vector storage, and sentence-transformers (all-MiniLM-L6-v2) for embeddings. Data validation uses Pydantic models with automatic serialization. The system supports dynamic model loading with differnt quantization settings, torch precision and device mapping.
10
+
11
+ We containerize the entire platform using Docker, enabling consistent deployment across diverse environments. The system is successfully deployed on Hugging Face Spaces with automatic port detection (7860) and frontend building. Key technical innovations include a modular assistant framework defined in TypeScript interfaces, environment-agnostic deployment that automatically adapts to different hosting platforms, and seamless integration between React components and FastAPI endpoints through RESTful APIs.
static/assets/index-af83f62d.js ADDED
The diff for this file is too large to render. See raw diff
 
static/assets/index-af83f62d.js.map ADDED
The diff for this file is too large to render. See raw diff
 
static/index.html CHANGED
@@ -5,7 +5,7 @@
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-ce47adf7.js"></script>
9
  <link rel="stylesheet" href="/assets/index-8f20e118.css">
10
  </head>
11
  <body>
 
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-af83f62d.js"></script>
9
  <link rel="stylesheet" href="/assets/index-8f20e118.css">
10
  </head>
11
  <body>