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 +172 -83
- frontend/src/pages/Playground.tsx +524 -409
- frontend/src/pages/Technology.tsx +168 -98
- software_implementation.tex +11 -0
- static/assets/index-af83f62d.js +0 -0
- static/assets/index-af83f62d.js.map +0 -0
- static/index.html +1 -1
frontend/src/components/ui/chat.tsx
CHANGED
|
@@ -1,48 +1,78 @@
|
|
| 1 |
-
import * as React from
|
| 2 |
-
import { cn } from
|
| 3 |
-
import { Button } from
|
| 4 |
-
import { Textarea } from
|
| 5 |
-
import { Send, Square, User, Bot } from
|
| 6 |
-
import ReactMarkdown from
|
| 7 |
|
| 8 |
-
import { AssistantInfo } from
|
| 9 |
|
| 10 |
export interface ChatProps extends React.HTMLAttributes<HTMLDivElement> {
|
| 11 |
messages: Array<{
|
| 12 |
-
id: string
|
| 13 |
-
role:
|
| 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 |
|
| 29 |
const scrollToBottom = () => {
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
React.useEffect(() => {
|
| 34 |
-
console.log(
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
return (
|
| 39 |
<div
|
| 40 |
-
className={cn(
|
| 41 |
ref={ref}
|
| 42 |
{...props}
|
| 43 |
>
|
| 44 |
{/* Messages */}
|
| 45 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 56 |
-
message.role ===
|
| 57 |
)}
|
| 58 |
>
|
| 59 |
{/* Avatar for assistant */}
|
| 60 |
-
{message.role !==
|
| 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 |
-
|
| 70 |
-
message.role ===
|
| 71 |
-
?
|
| 72 |
-
:
|
| 73 |
)}
|
| 74 |
>
|
| 75 |
<div className="text-xs opacity-70 flex items-center gap-2">
|
| 76 |
<span>
|
| 77 |
-
{message.role ===
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
"inline-block px-1.5 py-0.5 rounded text-[10px] font-medium ml-1",
|
| 84 |
-
message.assistantInfo.type ===
|
| 85 |
-
|
| 86 |
-
message.assistantInfo.type ===
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 }) =>
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
| 116 |
) : (
|
| 117 |
-
<code className="block bg-muted p-2 rounded text-xs font-mono overflow-x-auto whitespace-pre">
|
| 118 |
-
|
|
|
|
|
|
|
| 119 |
},
|
| 120 |
-
pre: ({ children }) =>
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
}}
|
| 128 |
>
|
| 129 |
{message.content}
|
| 130 |
</ReactMarkdown>
|
| 131 |
</div>
|
| 132 |
</div>
|
| 133 |
-
|
| 134 |
{/* Avatar for user */}
|
| 135 |
-
{message.role ===
|
| 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 ===
|
| 156 |
-
e.preventDefault()
|
| 157 |
-
handleSubmit(e as any)
|
| 158 |
}
|
| 159 |
}}
|
| 160 |
/>
|
| 161 |
{isGenerating ? (
|
| 162 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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
|
| 2 |
-
import { AssistantInfo } from
|
| 3 |
-
import { getPresetsFromConfigs } from
|
| 4 |
-
import { Button } from
|
| 5 |
-
import { Card } from
|
| 6 |
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from
|
| 7 |
-
import { Label } from
|
| 8 |
|
| 9 |
import {
|
| 10 |
AlertDialog,
|
|
@@ -15,10 +15,10 @@ import {
|
|
| 15 |
AlertDialogFooter,
|
| 16 |
AlertDialogHeader,
|
| 17 |
AlertDialogTitle,
|
| 18 |
-
} from
|
| 19 |
-
import { Chat } from
|
| 20 |
-
import { useChat } from
|
| 21 |
-
import {
|
| 22 |
Plus,
|
| 23 |
Trash2,
|
| 24 |
Save,
|
|
@@ -27,30 +27,30 @@ import {
|
|
| 27 |
BookOpen,
|
| 28 |
MessageSquare,
|
| 29 |
ChevronLeft,
|
| 30 |
-
ChevronRight
|
| 31 |
-
} from
|
| 32 |
|
| 33 |
// Import refactored components
|
| 34 |
-
import {
|
| 35 |
-
ModelParametersTab,
|
| 36 |
-
AssistantSelector,
|
| 37 |
-
SystemInstructionsTab,
|
| 38 |
-
DocumentsTab
|
| 39 |
-
} from
|
| 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:
|
| 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] =
|
| 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<{
|
| 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(
|
| 110 |
-
|
|
|
|
|
|
|
| 111 |
} catch (error) {
|
| 112 |
-
console.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 =
|
| 122 |
if (selectedAssistant) {
|
| 123 |
-
if (selectedAssistant.type ===
|
| 124 |
-
defaultName = selectedAssistant.name
|
| 125 |
-
} else if (selectedAssistant.type ===
|
| 126 |
-
defaultName = `My ${selectedAssistant.name}
|
| 127 |
} else {
|
| 128 |
-
defaultName = selectedAssistant.name +
|
| 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:
|
| 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:
|
| 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:
|
| 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:
|
| 225 |
-
name:
|
| 226 |
-
type:
|
| 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 ===
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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 ||
|
| 281 |
-
type: selectedAssistant?.type ||
|
| 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 ===
|
| 292 |
setSelectedAssistant({
|
| 293 |
-
id:
|
| 294 |
name: `Custom ${selectedAssistant.name}`,
|
| 295 |
-
type:
|
| 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(
|
| 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:
|
| 345 |
-
name: config.name ||
|
| 346 |
-
type:
|
| 347 |
-
})
|
| 348 |
-
|
| 349 |
// Clear the config after loading
|
| 350 |
-
localStorage.removeItem(
|
| 351 |
} catch (error) {
|
| 352 |
-
console.error(
|
| 353 |
-
localStorage.removeItem(
|
| 354 |
}
|
| 355 |
}
|
| 356 |
-
}, [])
|
| 357 |
|
| 358 |
// Debug logs for Session issue
|
| 359 |
useEffect(() => {
|
| 360 |
-
console.log(
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
useEffect(() => {
|
| 364 |
-
console.log(
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 384 |
-
|
|
|
|
|
|
|
| 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 ===
|
| 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(
|
| 397 |
-
m
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
)
|
| 401 |
|
| 402 |
for (const model of loadedLocalModels) {
|
| 403 |
try {
|
| 404 |
const response = await fetch(`${baseUrl}/unload-model`, {
|
| 405 |
-
method:
|
| 406 |
-
headers: {
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 437 |
-
headers: {
|
| 438 |
-
body: JSON.stringify({ model_name: pendingModelToLoad.model_name })
|
| 439 |
-
})
|
| 440 |
-
|
| 441 |
if (response.ok) {
|
| 442 |
-
console.log(
|
| 443 |
-
|
|
|
|
|
|
|
| 444 |
} else {
|
| 445 |
-
console.error(
|
|
|
|
|
|
|
| 446 |
// Revert to an API model if load failed
|
| 447 |
-
const apiModel = models.find(m => m.type ===
|
| 448 |
if (apiModel) {
|
| 449 |
-
setSelectedModel(apiModel.model_name)
|
| 450 |
}
|
| 451 |
}
|
| 452 |
} catch (error) {
|
| 453 |
-
console.error(
|
| 454 |
// Revert to an API model if error
|
| 455 |
-
const apiModel = models.find(m => m.type ===
|
| 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 ===
|
| 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(
|
| 481 |
-
|
|
|
|
|
|
|
| 482 |
for (const model of loadedLocalModels) {
|
| 483 |
try {
|
| 484 |
await fetch(`${baseUrl}/unload-model`, {
|
| 485 |
-
method:
|
| 486 |
-
headers: {
|
| 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 ===
|
| 516 |
-
const defaultModel = apiModel || data.models[0]
|
| 517 |
-
setSelectedModel(defaultModel.model_name)
|
| 518 |
}
|
| 519 |
}
|
| 520 |
} catch (err) {
|
| 521 |
-
console.error(
|
| 522 |
}
|
| 523 |
-
}
|
| 524 |
|
| 525 |
return (
|
| 526 |
<div className="h-screen bg-background flex">
|
| 527 |
{/* Chat Sessions Sidebar */}
|
| 528 |
-
<div
|
|
|
|
| 529 |
bg-background border-r flex-shrink-0 transition-all duration-300 ease-in-out
|
| 530 |
-
${sessionsCollapsed ?
|
| 531 |
-
`}
|
|
|
|
| 532 |
<div className="p-4 space-y-4 h-full">
|
| 533 |
-
<div
|
| 534 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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={
|
|
|
|
|
|
|
| 548 |
>
|
| 549 |
-
{sessionsCollapsed ?
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
</Button>
|
| 551 |
</div>
|
| 552 |
</div>
|
| 553 |
-
|
| 554 |
{sessionsCollapsed && (
|
| 555 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
<Plus className="h-4 w-4" />
|
| 557 |
</Button>
|
| 558 |
)}
|
| 559 |
<div className="space-y-2">
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 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 |
-
|
| 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
|
| 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 (
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 648 |
border-l bg-background flex-shrink-0 transition-all duration-300 ease-in-out
|
| 649 |
-
${configCollapsed ?
|
| 650 |
-
`}
|
|
|
|
| 651 |
<div className="p-4 space-y-4 h-full">
|
| 652 |
-
<div
|
| 653 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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={
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
>
|
| 662 |
-
{configCollapsed ?
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
</Button>
|
| 664 |
</div>
|
| 665 |
</div>
|
| 666 |
-
|
| 667 |
{configCollapsed && (
|
| 668 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 757 |
-
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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 ===
|
| 795 |
-
confirmSaveAssistant()
|
| 796 |
}
|
| 797 |
-
if (e.key ===
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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 ===
|
| 845 |
-
confirmRenameAssistant()
|
| 846 |
}
|
| 847 |
-
if (e.key ===
|
| 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
|
| 2 |
-
import { useNavigate } from
|
| 3 |
-
import { Button } from
|
| 4 |
-
import { Badge } from
|
| 5 |
-
import { Card, CardContent } from
|
| 6 |
-
import { Menu } from
|
| 7 |
|
| 8 |
export function Technology() {
|
| 9 |
-
const [activeTab, setActiveTab] = useState(
|
| 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(
|
| 26 |
-
e.currentTarget.style.display =
|
| 27 |
}}
|
| 28 |
/>
|
| 29 |
</div>
|
| 30 |
|
| 31 |
{/* Navigation Links */}
|
| 32 |
<div className="hidden md:flex items-center space-x-8">
|
| 33 |
-
<button
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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(
|
| 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 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 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
|
|
|
|
| 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 |
-
{[
|
| 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 |
-
?
|
| 85 |
-
:
|
| 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 ===
|
| 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
|
| 107 |
-
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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">
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
</div>
|
| 133 |
-
|
| 134 |
<div className="flex justify-between items-start">
|
| 135 |
-
<span className="text-lg font-bold text-gray-900">
|
|
|
|
|
|
|
| 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">
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
</div>
|
| 147 |
|
| 148 |
<div className="flex justify-between items-start">
|
| 149 |
-
<span className="text-lg font-bold text-gray-900">
|
|
|
|
|
|
|
| 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>
|
|
|
|
|
|
|
|
|
|
| 153 |
</div>
|
| 154 |
</div>
|
| 155 |
|
| 156 |
<div className="flex justify-between items-start">
|
| 157 |
-
<span className="text-lg font-bold text-gray-900">
|
|
|
|
|
|
|
| 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 ===
|
| 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
|
| 186 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 192 |
</li>
|
| 193 |
<li className="flex items-start">
|
| 194 |
<span className="text-purple-600 mr-2">•</span>
|
| 195 |
-
|
|
|
|
| 196 |
</li>
|
| 197 |
<li className="flex items-start">
|
| 198 |
<span className="text-purple-600 mr-2">•</span>
|
| 199 |
-
|
|
|
|
| 200 |
</li>
|
| 201 |
<li className="flex items-start">
|
| 202 |
<span className="text-purple-600 mr-2">•</span>
|
| 203 |
-
|
|
|
|
| 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">
|
|
|
|
|
|
|
| 214 |
<div className="text-base text-gray-700 text-right">
|
| 215 |
-
<div>
|
| 216 |
-
<div>
|
|
|
|
| 217 |
</div>
|
| 218 |
</div>
|
| 219 |
-
|
| 220 |
<div className="flex justify-between items-start">
|
| 221 |
-
<span className="text-lg font-semibold text-gray-900">
|
|
|
|
|
|
|
| 222 |
<div className="text-base text-gray-700 text-right">
|
| 223 |
-
<div>
|
| 224 |
-
<div>
|
| 225 |
-
<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">
|
|
|
|
|
|
|
| 232 |
<div className="text-base text-gray-700 text-right">
|
| 233 |
-
<div>
|
| 234 |
-
<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">
|
|
|
|
|
|
|
| 241 |
<div className="text-base text-gray-700 text-right">
|
| 242 |
-
<div>
|
| 243 |
-
<div>
|
| 244 |
-
<div>
|
| 245 |
</div>
|
| 246 |
</div>
|
| 247 |
|
| 248 |
<div className="flex justify-between items-start">
|
| 249 |
-
<span className="text-lg font-semibold text-gray-900">
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
| 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:
|
| 295 |
-
{ label:
|
| 296 |
-
{
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
].map((row, index) => (
|
| 301 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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(
|
| 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">
|
| 334 |
-
|
|
|
|
|
|
|
| 335 |
<div className="bg-white rounded-2xl shadow-xl p-8 text-center">
|
| 336 |
<div className="relative max-w-4xl mx-auto">
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 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>
|