mirror of https://github.com/kortix-ai/suna.git
improve UI of models selection
This commit is contained in:
parent
5bb5300afe
commit
99d41d8c8d
|
@ -3,7 +3,7 @@
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Send, Square, Loader2, File, Upload, X, Paperclip, FileText, ChevronDown } from "lucide-react";
|
import { Send, Square, Loader2, File, Upload, X, Paperclip, FileText, ChevronDown, Cpu } from "lucide-react";
|
||||||
import { createClient } from "@/lib/supabase/client";
|
import { createClient } from "@/lib/supabase/client";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
@ -26,7 +26,6 @@ const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
||||||
|
|
||||||
// Local storage keys
|
// Local storage keys
|
||||||
const STORAGE_KEY_MODEL = 'suna-preferred-model';
|
const STORAGE_KEY_MODEL = 'suna-preferred-model';
|
||||||
const STORAGE_KEY_THINKING = 'suna-enable-thinking';
|
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
onSubmit: (message: string, options?: { model_name?: string; enable_thinking?: boolean }) => void;
|
onSubmit: (message: string, options?: { model_name?: string; enable_thinking?: boolean }) => void;
|
||||||
|
@ -63,52 +62,39 @@ export function ChatInput({
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const [inputValue, setInputValue] = useState(value || "");
|
const [inputValue, setInputValue] = useState(value || "");
|
||||||
const [selectedModel, setSelectedModel] = useState("sonnet-3.7");
|
const [selectedModel, setSelectedModel] = useState("sonnet-3.7");
|
||||||
const [enableThinking, setEnableThinking] = useState(false);
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
|
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||||
|
|
||||||
// Load saved preferences from localStorage on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
try {
|
try {
|
||||||
// Load selected model
|
|
||||||
const savedModel = localStorage.getItem(STORAGE_KEY_MODEL);
|
const savedModel = localStorage.getItem(STORAGE_KEY_MODEL);
|
||||||
if (savedModel) {
|
if (savedModel) {
|
||||||
setSelectedModel(savedModel);
|
setSelectedModel(savedModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load thinking preference
|
|
||||||
const savedThinking = localStorage.getItem(STORAGE_KEY_THINKING);
|
|
||||||
if (savedThinking === 'true') {
|
|
||||||
setEnableThinking(true);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load preferences from localStorage:', error);
|
console.warn('Failed to load preferences from localStorage:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Allow controlled or uncontrolled usage
|
|
||||||
const isControlled = value !== undefined && onChange !== undefined;
|
const isControlled = value !== undefined && onChange !== undefined;
|
||||||
|
|
||||||
// Update local state if controlled and value changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isControlled && value !== inputValue) {
|
if (isControlled && value !== inputValue) {
|
||||||
setInputValue(value);
|
setInputValue(value);
|
||||||
}
|
}
|
||||||
}, [value, isControlled, inputValue]);
|
}, [value, isControlled, inputValue]);
|
||||||
|
|
||||||
// Auto-focus on textarea when component loads
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoFocus && textareaRef.current) {
|
if (autoFocus && textareaRef.current) {
|
||||||
textareaRef.current.focus();
|
textareaRef.current.focus();
|
||||||
}
|
}
|
||||||
}, [autoFocus]);
|
}, [autoFocus]);
|
||||||
|
|
||||||
// Adjust textarea height based on content
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const textarea = textareaRef.current;
|
const textarea = textareaRef.current;
|
||||||
if (!textarea) return;
|
if (!textarea) return;
|
||||||
|
@ -121,34 +107,15 @@ export function ChatInput({
|
||||||
|
|
||||||
adjustHeight();
|
adjustHeight();
|
||||||
|
|
||||||
// Adjust on window resize too
|
|
||||||
window.addEventListener('resize', adjustHeight);
|
window.addEventListener('resize', adjustHeight);
|
||||||
return () => window.removeEventListener('resize', adjustHeight);
|
return () => window.removeEventListener('resize', adjustHeight);
|
||||||
}, [inputValue]);
|
}, [inputValue]);
|
||||||
|
|
||||||
const handleModelChange = (model: string) => {
|
const handleModelChange = (model: string) => {
|
||||||
setSelectedModel(model);
|
setSelectedModel(model);
|
||||||
// Save to localStorage
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
localStorage.setItem(STORAGE_KEY_MODEL, model);
|
localStorage.setItem(STORAGE_KEY_MODEL, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset thinking when changing away from sonnet-3.7
|
|
||||||
if (model !== "sonnet-3.7") {
|
|
||||||
setEnableThinking(false);
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
localStorage.setItem(STORAGE_KEY_THINKING, 'false');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleThinking = () => {
|
|
||||||
const newValue = !enableThinking;
|
|
||||||
setEnableThinking(newValue);
|
|
||||||
// Save to localStorage
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
localStorage.setItem(STORAGE_KEY_THINKING, newValue.toString());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
@ -162,7 +129,6 @@ export function ChatInput({
|
||||||
|
|
||||||
let message = inputValue;
|
let message = inputValue;
|
||||||
|
|
||||||
// Add file information to the message if files were uploaded
|
|
||||||
if (uploadedFiles.length > 0) {
|
if (uploadedFiles.length > 0) {
|
||||||
const fileInfo = uploadedFiles.map(file =>
|
const fileInfo = uploadedFiles.map(file =>
|
||||||
`[Uploaded file: ${file.name} (${formatFileSize(file.size)}) at ${file.path}]`
|
`[Uploaded file: ${file.name} (${formatFileSize(file.size)}) at ${file.path}]`
|
||||||
|
@ -170,16 +136,22 @@ export function ChatInput({
|
||||||
message = message ? `${message}\n\n${fileInfo}` : fileInfo;
|
message = message ? `${message}\n\n${fileInfo}` : fileInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let baseModelName = selectedModel;
|
||||||
|
let thinkingEnabled = false;
|
||||||
|
if (selectedModel === "sonnet-3.7-thinking") {
|
||||||
|
baseModelName = "sonnet-3.7";
|
||||||
|
thinkingEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
onSubmit(message, {
|
onSubmit(message, {
|
||||||
model_name: selectedModel,
|
model_name: baseModelName,
|
||||||
enable_thinking: enableThinking
|
enable_thinking: thinkingEnabled
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isControlled) {
|
if (!isControlled) {
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the uploaded files after sending
|
|
||||||
setUploadedFiles([]);
|
setUploadedFiles([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -236,7 +208,6 @@ export function ChatInput({
|
||||||
const files = Array.from(event.target.files);
|
const files = Array.from(event.target.files);
|
||||||
await uploadFiles(files);
|
await uploadFiles(files);
|
||||||
|
|
||||||
// Reset the input
|
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -252,11 +223,9 @@ export function ChatInput({
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a FormData object
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
// Upload to workspace root by default
|
|
||||||
const uploadPath = `/workspace/${file.name}`;
|
const uploadPath = `/workspace/${file.name}`;
|
||||||
formData.append('path', uploadPath);
|
formData.append('path', uploadPath);
|
||||||
|
|
||||||
|
@ -267,7 +236,6 @@ export function ChatInput({
|
||||||
throw new Error('No access token available');
|
throw new Error('No access token available');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload using FormData
|
|
||||||
const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files`, {
|
const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -280,7 +248,6 @@ export function ChatInput({
|
||||||
throw new Error(`Upload failed: ${response.statusText}`);
|
throw new Error(`Upload failed: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to uploaded files
|
|
||||||
newUploadedFiles.push({
|
newUploadedFiles.push({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
path: uploadPath,
|
path: uploadPath,
|
||||||
|
@ -290,7 +257,6 @@ export function ChatInput({
|
||||||
toast.success(`File uploaded: ${file.name}`);
|
toast.success(`File uploaded: ${file.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the uploaded files state
|
|
||||||
setUploadedFiles(prev => [...prev, ...newUploadedFiles]);
|
setUploadedFiles(prev => [...prev, ...newUploadedFiles]);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -334,9 +300,9 @@ export function ChatInput({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map of model display names
|
const modelDisplayNames: { [key: string]: string } = {
|
||||||
const modelDisplayNames = {
|
|
||||||
"sonnet-3.7": "Sonnet 3.7",
|
"sonnet-3.7": "Sonnet 3.7",
|
||||||
|
"sonnet-3.7-thinking": "Sonnet 3.7 (Thinking)",
|
||||||
"gpt-4.1": "GPT-4.1",
|
"gpt-4.1": "GPT-4.1",
|
||||||
"gemini-flash-2.5": "Gemini Flash 2.5"
|
"gemini-flash-2.5": "Gemini Flash 2.5"
|
||||||
};
|
};
|
||||||
|
@ -393,54 +359,6 @@ export function ChatInput({
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<div className="flex items-center px-3 py-3">
|
<div className="flex items-center px-3 py-3">
|
||||||
{/* Left side - Model selector and Think button */}
|
|
||||||
{!isAgentRunning && (
|
|
||||||
<div className="flex items-center gap-2 mr-3">
|
|
||||||
{/* Model selector button with dropdown */}
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 px-3 text-sm text-gray-300 gap-1 border-0 shadow-none bg-transparent hover:bg-gray-800"
|
|
||||||
>
|
|
||||||
{modelDisplayNames[selectedModel as keyof typeof modelDisplayNames] || selectedModel}
|
|
||||||
<ChevronDown className="h-3 w-3 ml-1 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start" className="bg-gray-900 border-gray-800 text-gray-300">
|
|
||||||
<DropdownMenuItem onClick={() => handleModelChange("sonnet-3.7")} className="hover:bg-gray-800">
|
|
||||||
Sonnet 3.7
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handleModelChange("gpt-4.1")} className="hover:bg-gray-800">
|
|
||||||
GPT-4.1
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handleModelChange("gemini-flash-2.5")} className="hover:bg-gray-800">
|
|
||||||
Gemini Flash 2.5
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
{/* Think button - only for Sonnet 3.7 */}
|
|
||||||
{selectedModel === "sonnet-3.7" && (
|
|
||||||
<Button
|
|
||||||
variant={enableThinking ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
className={cn(
|
|
||||||
"h-8 px-3 text-sm",
|
|
||||||
enableThinking
|
|
||||||
? "bg-gray-700 text-gray-200 hover:bg-gray-600"
|
|
||||||
: "text-gray-300 border-0 hover:bg-gray-800"
|
|
||||||
)}
|
|
||||||
onClick={toggleThinking}
|
|
||||||
>
|
|
||||||
Think
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Input field */}
|
|
||||||
<div className="relative flex-1 flex items-center">
|
<div className="relative flex-1 flex items-center">
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
|
@ -461,9 +379,68 @@ export function ChatInput({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side buttons */}
|
|
||||||
<div className="flex items-center gap-3 ml-3">
|
<div className="flex items-center gap-3 ml-3">
|
||||||
{/* Attachment button */}
|
{!isAgentRunning && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
"h-8 w-8 rounded-full p-0 hover:bg-gray-800",
|
||||||
|
selectedModel === "sonnet-3.7" ? "text-purple-400" :
|
||||||
|
selectedModel === "sonnet-3.7-thinking" ? "text-violet-400" :
|
||||||
|
selectedModel === "gpt-4.1" ? "text-green-400" :
|
||||||
|
selectedModel === "gemini-flash-2.5" ? "text-blue-400" :
|
||||||
|
"text-gray-400"
|
||||||
|
)}
|
||||||
|
aria-label="Select model"
|
||||||
|
>
|
||||||
|
<Cpu className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="bg-gray-900 border-gray-800 text-gray-300">
|
||||||
|
<DropdownMenuItem onClick={() => handleModelChange("sonnet-3.7")} className={cn(
|
||||||
|
"hover:bg-gray-800 flex items-center justify-between",
|
||||||
|
selectedModel === "sonnet-3.7" && "text-purple-400"
|
||||||
|
)}>
|
||||||
|
<span>Sonnet 3.7</span>
|
||||||
|
{selectedModel === "sonnet-3.7" && <span className="ml-2">✓</span>}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleModelChange("sonnet-3.7-thinking")} className={cn(
|
||||||
|
"hover:bg-gray-800 flex items-center justify-between",
|
||||||
|
selectedModel === "sonnet-3.7-thinking" && "text-violet-400"
|
||||||
|
)}>
|
||||||
|
<span>Sonnet 3.7 (Thinking)</span>
|
||||||
|
{selectedModel === "sonnet-3.7-thinking" && <span className="ml-2">✓</span>}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleModelChange("gpt-4.1")} className={cn(
|
||||||
|
"hover:bg-gray-800 flex items-center justify-between",
|
||||||
|
selectedModel === "gpt-4.1" && "text-green-400"
|
||||||
|
)}>
|
||||||
|
<span>GPT-4.1</span>
|
||||||
|
{selectedModel === "gpt-4.1" && <span className="ml-2">✓</span>}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleModelChange("gemini-flash-2.5")} className={cn(
|
||||||
|
"hover:bg-gray-800 flex items-center justify-between",
|
||||||
|
selectedModel === "gemini-flash-2.5" && "text-blue-400"
|
||||||
|
)}>
|
||||||
|
<span>Gemini Flash 2.5</span>
|
||||||
|
{selectedModel === "gemini-flash-2.5" && <span className="ml-2">✓</span>}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="bg-gray-900 text-gray-300 border-gray-800">
|
||||||
|
<p>Model: {modelDisplayNames[selectedModel as keyof typeof modelDisplayNames] || selectedModel}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
@ -492,7 +469,6 @@ export function ChatInput({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
{/* Hidden file input */}
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
|
@ -501,7 +477,6 @@ export function ChatInput({
|
||||||
multiple
|
multiple
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Send button */}
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|
Loading…
Reference in New Issue