mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
feat: Add mini model menu (#686)
This commit is contained in:
@@ -1,8 +1,54 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useModel } from "./settings/models/ModelContext";
|
||||
import { useRecentModels } from "./settings/models/RecentModels"; // Hook for recent models
|
||||
import { ChevronUp, ChevronDown, Sliders } from "lucide-react";
|
||||
import { ModelRadioList } from "./settings/models/ModelRadioList";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function BottomMenu({ hasMessages }) {
|
||||
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
|
||||
const { currentModel } = useModel();
|
||||
const { recentModels } = useRecentModels(); // Get recent models
|
||||
const navigate = useNavigate();
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Add effect to handle clicks outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsModelMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isModelMenuOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isModelMenuOpen]);
|
||||
|
||||
// Add effect to handle Escape key
|
||||
useEffect(() => {
|
||||
const handleEsc = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsModelMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isModelMenuOpen) {
|
||||
window.addEventListener('keydown', handleEsc);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleEsc);
|
||||
};
|
||||
}, [isModelMenuOpen]);
|
||||
|
||||
return (
|
||||
<div className="flex relative border-t dark:border-gray-700 text-bottom-menu dark:text-bottom-menu-dark pl-[15px] text-[10px] h-[30px] leading-[30px] align-middle bg-bottom-menu-background dark:bg-bottom-menu-background-dark rounded-b-2xl">
|
||||
<div className="flex justify-between relative border-t dark:border-gray-700 text-bottom-menu dark:text-bottom-menu-dark pl-[15px] text-[10px] h-[30px] leading-[30px] align-middle bg-bottom-menu-background dark:bg-bottom-menu-background-dark rounded-b-2xl">
|
||||
{/* Directory Chooser - Always visible */}
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={async () => {
|
||||
@@ -16,6 +62,72 @@ export default function BottomMenu({ hasMessages }) {
|
||||
>
|
||||
Working in {window.appConfig.get("GOOSE_WORKING_DIR")}
|
||||
</span>
|
||||
|
||||
{/* Model Selector Dropdown - Only in development */}
|
||||
{process.env.NODE_ENV === "development" && (
|
||||
<div className="relative flex items-center ml-auto mr-4" ref={dropdownRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer"
|
||||
onClick={() => setIsModelMenuOpen(!isModelMenuOpen)}
|
||||
>
|
||||
<span>{currentModel?.name || "Select Model"}</span>
|
||||
{isModelMenuOpen ? (
|
||||
<ChevronUp className="w-4 h-4 ml-1" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4 ml-1" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isModelMenuOpen && (
|
||||
<div className="absolute bottom-[30px] right-0 w-[300px] bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-lg shadow-lg">
|
||||
<div className="p-2">
|
||||
<ModelRadioList
|
||||
className="divide-y divide-gray-100 dark:divide-gray-700"
|
||||
renderItem={({ model, isSelected, onSelect }) => (
|
||||
<label key={model.name} className="block cursor-pointer">
|
||||
<div className="flex items-center justify-between p-3 transition-colors
|
||||
hover:text-gray-900 dark:hover:text-white"
|
||||
onClick={onSelect}>
|
||||
<div>
|
||||
<p className="text-sm font-semibold">{model.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{model.provider}</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="radio"
|
||||
name="recentModels"
|
||||
value={model.name}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="h-4 w-4 rounded-full border border-gray-400 dark:border-gray-500
|
||||
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
|
||||
peer-checked:bg-white dark:peer-checked:bg-black
|
||||
transition-all duration-200 ease-in-out">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700
|
||||
border-t-2 border-gray-200 dark:border-gray-600"
|
||||
onClick={() => {
|
||||
setIsModelMenuOpen(false);
|
||||
navigate("/settings");
|
||||
}}
|
||||
>
|
||||
<span className="text-sm">Tools and Settings</span>
|
||||
<Sliders className="w-5 h-5 ml-2 rotate-90" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Model } from "./ModelContext";
|
||||
import { useModel } from "./ModelContext";
|
||||
import { useHandleModelSelection } from "./utils";
|
||||
import { useRecentModels } from "./RecentModels";
|
||||
|
||||
interface ModelRadioListProps {
|
||||
renderItem: (props: {
|
||||
model: Model,
|
||||
isSelected: boolean,
|
||||
onSelect: () => void,
|
||||
}) => React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ModelRadioList({ renderItem, className = "" }: ModelRadioListProps) {
|
||||
const { recentModels } = useRecentModels();
|
||||
const { currentModel } = useModel();
|
||||
const handleModelSelection = useHandleModelSelection();
|
||||
const [selectedModel, setSelectedModel] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentModel) {
|
||||
setSelectedModel(currentModel.name);
|
||||
}
|
||||
}, [currentModel]);
|
||||
|
||||
const handleRadioChange = async (model: Model) => {
|
||||
if (selectedModel === model.name) {
|
||||
console.log(`Model "${model.name}" is already active.`);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedModel(model.name);
|
||||
await handleModelSelection(model, "ModelList");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{recentModels.map((model) => (
|
||||
renderItem({
|
||||
model,
|
||||
isSelected: selectedModel === model.name,
|
||||
onSelect: () => handleRadioChange(model),
|
||||
})
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Clock } from 'lucide-react';
|
||||
import { Model } from "./ModelContext"
|
||||
import { useHandleModelSelection } from "./utils";
|
||||
import { useModel } from "./ModelContext";
|
||||
import { ModelRadioList } from "./ModelRadioList";
|
||||
|
||||
const MAX_RECENT_MODELS = 3
|
||||
|
||||
@@ -134,58 +135,34 @@ export function RecentModels() {
|
||||
}
|
||||
|
||||
export function RecentModelsRadio() {
|
||||
const { recentModels } = useRecentModels(); // Access recent models
|
||||
const handleModelSelection = useHandleModelSelection(); // Access the model selection handler
|
||||
const { currentModel } = useModel(); // Get the current selected model
|
||||
const [selectedModel, setSelectedModel] = useState<string | null>(null); // Track the currently selected model
|
||||
|
||||
// Initialize selectedModel with the currentModel on component mount
|
||||
useEffect(() => {
|
||||
if (currentModel) {
|
||||
setSelectedModel(currentModel.name);
|
||||
}
|
||||
}, [currentModel]);
|
||||
|
||||
const handleRadioChange = async (model: Model) => {
|
||||
if (selectedModel === model.name) {
|
||||
// Display feedback for already selected model
|
||||
console.log(`Model "${model.name}" is already active.`);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedModel(model.name); // Update the selected model locally
|
||||
await handleModelSelection(model, "RecentModels"); // Switch the model using the handler
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold">Recent Models</h2>
|
||||
{recentModels.map((model) => (
|
||||
<label
|
||||
key={model.name}
|
||||
className="flex items-center justify-between p-4 cursor-pointer"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">{model.name}</p>
|
||||
<p className="font-medium">{model.provider}</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="radio"
|
||||
name="recentModels"
|
||||
value={model.name}
|
||||
checked={selectedModel === model.name}
|
||||
onChange={() => handleRadioChange(model)}
|
||||
className="peer sr-only" // Hide the default radio button
|
||||
/>
|
||||
<div className="h-4 w-4 rounded-full border border-gray-400 dark:border-gray-500
|
||||
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
|
||||
peer-checked:bg-white dark:peer-checked:bg-black
|
||||
transition-all duration-200 ease-in-out">
|
||||
<ModelRadioList
|
||||
renderItem={({ model, isSelected, onSelect }) => (
|
||||
<label key={model.name} className="flex items-center justify-between p-4 cursor-pointer">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">{model.name}</p>
|
||||
<p className="font-medium">{model.provider}</p>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="radio"
|
||||
name="recentModels"
|
||||
value={model.name}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="h-4 w-4 rounded-full border border-gray-400 dark:border-gray-500
|
||||
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
|
||||
peer-checked:bg-white dark:peer-checked:bg-black
|
||||
transition-all duration-200 ease-in-out">
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export function ToastSuccessModelSwitch(model: Model) {
|
||||
<div>Switched to {model.name}</div>
|
||||
</div>,
|
||||
{
|
||||
position: "bottom-right",
|
||||
position: "top-right",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: true,
|
||||
@@ -25,7 +25,7 @@ export function ToastFailureGeneral(msg?: string) {
|
||||
<div>{msg}</div>
|
||||
</div>,
|
||||
{
|
||||
position: "bottom-right",
|
||||
position: "top-right",
|
||||
autoClose: 3000,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: true,
|
||||
|
||||
Reference in New Issue
Block a user