diff --git a/ui/desktop/src/components/BottomMenu.tsx b/ui/desktop/src/components/BottomMenu.tsx index 67417847a3..ba61031577 100644 --- a/ui/desktop/src/components/BottomMenu.tsx +++ b/ui/desktop/src/components/BottomMenu.tsx @@ -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(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 ( -
+
+ {/* Directory Chooser - Always visible */} { @@ -16,6 +62,72 @@ export default function BottomMenu({ hasMessages }) { > Working in {window.appConfig.get("GOOSE_WORKING_DIR")} + + {/* Model Selector Dropdown - Only in development */} + {process.env.NODE_ENV === "development" && ( +
+
setIsModelMenuOpen(!isModelMenuOpen)} + > + {currentModel?.name || "Select Model"} + {isModelMenuOpen ? ( + + ) : ( + + )} +
+ + {/* Dropdown Menu */} + {isModelMenuOpen && ( +
+
+ ( + + )} + /> +
{ + setIsModelMenuOpen(false); + navigate("/settings"); + }} + > + Tools and Settings + +
+
+
+ )} +
+ )}
); } diff --git a/ui/desktop/src/components/settings/models/ModelRadioList.tsx b/ui/desktop/src/components/settings/models/ModelRadioList.tsx new file mode 100644 index 0000000000..03cbac615f --- /dev/null +++ b/ui/desktop/src/components/settings/models/ModelRadioList.tsx @@ -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(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 ( +
+ {recentModels.map((model) => ( + renderItem({ + model, + isSelected: selectedModel === model.name, + onSelect: () => handleRadioChange(model), + }) + ))} +
+ ); +} \ No newline at end of file diff --git a/ui/desktop/src/components/settings/models/RecentModels.tsx b/ui/desktop/src/components/settings/models/RecentModels.tsx index 2a9347afef..f1914de87b 100644 --- a/ui/desktop/src/components/settings/models/RecentModels.tsx +++ b/ui/desktop/src/components/settings/models/RecentModels.tsx @@ -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(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 (

Recent Models

- {recentModels.map((model) => ( -
, { - position: "bottom-right", + position: "top-right", autoClose: 5000, hideProgressBar: true, closeOnClick: true, @@ -25,7 +25,7 @@ export function ToastFailureGeneral(msg?: string) {
{msg}
, { - position: "bottom-right", + position: "top-right", autoClose: 3000, hideProgressBar: true, closeOnClick: true,