From 8b35f6102cb482be5edbf4967ed3a722fc3dfa56 Mon Sep 17 00:00:00 2001 From: lily-de <119957291+lily-de@users.noreply.github.com> Date: Wed, 22 Jan 2025 20:40:03 -0800 Subject: [PATCH] feat: welcome page (#696) --- ui/desktop/src/ChatWindow.tsx | 66 +--- ui/desktop/src/components/MoreMenu.tsx | 3 + .../src/components/chat_window/ChatRoutes.tsx | 2 + .../settings/providers/Provider.tsx | 2 +- .../settings/providers/ProviderGrid.tsx | 293 ++++++++++++++++++ ui/desktop/src/components/ui/Tooltip.tsx | 41 ++- .../welcome_screen/WelcomeModal.tsx | 62 ---- .../welcome_screen/WelcomeScreen.tsx | 33 ++ 8 files changed, 371 insertions(+), 131 deletions(-) create mode 100644 ui/desktop/src/components/settings/providers/ProviderGrid.tsx delete mode 100644 ui/desktop/src/components/welcome_screen/WelcomeModal.tsx create mode 100644 ui/desktop/src/components/welcome_screen/WelcomeScreen.tsx diff --git a/ui/desktop/src/ChatWindow.tsx b/ui/desktop/src/ChatWindow.tsx index 3ee0acf914..883db05a3b 100644 --- a/ui/desktop/src/ChatWindow.tsx +++ b/ui/desktop/src/ChatWindow.tsx @@ -9,7 +9,6 @@ import GooseMessage from './components/GooseMessage'; import Input from './components/Input'; import LoadingGoose from './components/LoadingGoose'; import MoreMenu from './components/MoreMenu'; -import Splash from './components/Splash'; import { Card } from './components/ui/card'; import { ScrollArea } from './components/ui/scroll-area'; import UserMessage from './components/UserMessage'; @@ -18,12 +17,13 @@ import { askAi } from './utils/askAI'; import { getStoredModel, Provider } from './utils/providerUtils'; import { ChatLayout } from './components/chat_window/ChatLayout'; import { ChatRoutes } from './components/chat_window/ChatRoutes'; -import { WelcomeModal } from './components/welcome_screen/WelcomeModal'; +import { WelcomeScreen } from './components/welcome_screen/WelcomeScreen'; import { getStoredProvider, initializeSystem } from './utils/providerUtils'; import { useModel } from './components/settings/models/ModelContext'; import { useRecentModels } from './components/settings/models/RecentModels'; import { createSelectedModel } from './components/settings/models/utils'; import { getDefaultModel } from './components/settings/models/hardcoded_stuff'; +import Splash from './components/Splash'; declare global { interface Window { @@ -380,6 +380,11 @@ export default function ChatWindow() { window.electron.logInfo('ChatWindow loaded'); + // Fix the handleSubmit function syntax + const handleSubmit = () => { + setShowWelcomeModal(false); + }; + useEffect(() => { // Check if we already have a provider set const config = window.electron.getConfig(); @@ -409,43 +414,6 @@ export default function ChatWindow() { return response; }; - const handleModalSubmit = async (apiKey: string) => { - try { - const trimmedKey = apiKey.trim(); - - if (!selectedProvider) { - throw new Error('No provider selected'); - } - - // Store the API key - const secretKey = `${selectedProvider.id.toUpperCase()}_API_KEY`; - await storeSecret(secretKey, trimmedKey); - - // Initialize the system with the selected provider - await initializeSystem(selectedProvider.id, null); - - // get the default model - const modelName = getDefaultModel(selectedProvider.id); - - // create model object - const model = createSelectedModel(selectedProvider.id, modelName); - - // Call the context's switchModel to track the set model state in the front end - switchModel(model); - - // Keep track of the recently used models - addRecentModel(model); - - // Save provider selection and close modal - localStorage.setItem('GOOSE_PROVIDER', selectedProvider.id); - console.log('set up provider with default model', selectedProvider.id, modelName); - setShowWelcomeModal(false); - } catch (error) { - console.error('Failed to setup provider:', error); - throw error; - } - }; - // Initialize system on load if we have a stored provider useEffect(() => { const setupStoredProvider = async () => { @@ -480,6 +448,12 @@ export default function ChatWindow() { setupStoredProvider(); }, []); + // Render WelcomeScreen at root level if showing + if (showWelcomeModal) { + return ; + } + + // Only render ChatLayout if not showing welcome screen return (
@@ -491,20 +465,6 @@ export default function ChatWindow() { setProgressMessage={setProgressMessage} setWorking={setWorking} /> - {/* - - */} - {showWelcomeModal && ( - - )}
); diff --git a/ui/desktop/src/components/MoreMenu.tsx b/ui/desktop/src/components/MoreMenu.tsx index ddcbde898d..a595a79120 100644 --- a/ui/desktop/src/components/MoreMenu.tsx +++ b/ui/desktop/src/components/MoreMenu.tsx @@ -4,6 +4,9 @@ import { FaMoon, FaSun } from 'react-icons/fa'; import VertDots from './ui/VertDots'; import { useNavigate } from 'react-router-dom'; import { More } from './icons'; +import { Settings, Grid, MessageSquare } from 'lucide-react'; +import { Button } from './ui/button'; + interface VersionInfo { current_version: string; available_versions: string[]; diff --git a/ui/desktop/src/components/chat_window/ChatRoutes.tsx b/ui/desktop/src/components/chat_window/ChatRoutes.tsx index f692a56267..19365ebf76 100644 --- a/ui/desktop/src/components/chat_window/ChatRoutes.tsx +++ b/ui/desktop/src/components/chat_window/ChatRoutes.tsx @@ -4,6 +4,7 @@ import { ChatContent } from '../../ChatWindow'; import Settings from '../settings/Settings'; import MoreModelsSettings from '../settings/models/MoreModels'; import ConfigureProviders from '../settings/providers/ConfigureProviders'; +import { WelcomeScreen } from '../welcome_screen/WelcomeScreen'; export const ChatRoutes = ({ chats, @@ -31,6 +32,7 @@ export const ChatRoutes = ({ } /> } /> } /> + } /> } /> ); diff --git a/ui/desktop/src/components/settings/providers/Provider.tsx b/ui/desktop/src/components/settings/providers/Provider.tsx index 424f6a47ce..82956604bb 100644 --- a/ui/desktop/src/components/settings/providers/Provider.tsx +++ b/ui/desktop/src/components/settings/providers/Provider.tsx @@ -34,7 +34,7 @@ function ConfirmationModal({ message, onConfirm, onCancel }) { } // Utility Functions -function getProviderDescription(provider) { +export function getProviderDescription(provider) { const descriptions = { OpenAI: 'Access GPT-4, GPT-3.5 Turbo, and other OpenAI models', Anthropic: 'Access Claude and other Anthropic models', diff --git a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx new file mode 100644 index 0000000000..61722a7ef2 --- /dev/null +++ b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx @@ -0,0 +1,293 @@ +import React from 'react'; +import { Check, Plus } from 'lucide-react'; +import { Button } from '../../ui/button'; +import { supported_providers, required_keys, provider_aliases } from '../models/hardcoded_stuff'; +import { useActiveKeys } from '../api_keys/ActiveKeysContext'; +import { getProviderDescription } from './Provider'; +import { ProviderSetupModal } from '../ProviderSetupModal'; +import { useModel } from '../models/ModelContext'; +import { useRecentModels } from '../models/RecentModels'; +import { createSelectedModel } from '../models/utils'; +import { getDefaultModel } from '../models/hardcoded_stuff'; +import { initializeSystem } from '../../../utils/providerUtils'; +import { getApiUrl, getSecretKey } from '../../../config'; +import { toast } from 'react-toastify'; +import { getActiveProviders } from '../api_keys/utils'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/Tooltip'; +import { useNavigate } from 'react-router-dom'; + +interface ProviderCardProps { + name: string; + description: string; + isConfigured: boolean; + onConfigure: () => void; + onAddKeys: () => void; + isSelected: boolean; + onSelect: () => void; +} + +function getArticle(word: string): string { + return 'aeiouAEIOU'.indexOf(word[0]) >= 0 ? 'an' : 'a'; +} + +function ProviderCard({ + name, + description, + isConfigured, + onConfigure, + onAddKeys, + isSelected, + onSelect, +}: ProviderCardProps) { + return ( +
isConfigured && onSelect()} + className={`relative bg-white dark:bg-gray-800 rounded-lg border + ${ + isSelected + ? 'border-blue-500 dark:border-blue-400 shadow-[0_0_0_1px] shadow-blue-500/50' + : 'border-gray-200 dark:border-gray-700' + } + p-3 transition-all duration-200 h-[140px] overflow-hidden + ${isConfigured ? 'cursor-pointer hover:border-blue-400 dark:hover:border-blue-300' : ''} + `} + > +
+
+

+ {name} +

+ {isConfigured && ( + + + +
+ +
+
+ +

+ You have {getArticle(name)} {name} API Key set in your environment +

+
+
+
+ )} +
+
+ +

+ {description} +

+ +
+ {!isConfigured && ( + + )} +
+
+ ); +} + +interface ProviderGridProps { + onSubmit?: () => void; +} + +export function ProviderGrid({ onSubmit }: ProviderGridProps) { + const { activeKeys, setActiveKeys } = useActiveKeys(); + const [selectedId, setSelectedId] = React.useState(null); + const [showSetupModal, setShowSetupModal] = React.useState(false); + const { switchModel } = useModel(); + const { addRecentModel } = useRecentModels(); + const navigate = useNavigate(); + + const providers = React.useMemo(() => { + return supported_providers.map((providerName) => { + const alias = + provider_aliases.find((p) => p.provider === providerName)?.alias || + providerName.toLowerCase(); + const isConfigured = activeKeys.includes(providerName); + + return { + id: alias, + name: providerName, + isConfigured, + description: getProviderDescription(providerName), + }; + }); + }, [activeKeys]); + + const handleConfigure = async (provider) => { + const providerId = provider.id.toLowerCase(); + await initializeSystem(providerId, null); + + const modelName = getDefaultModel(providerId); + const model = createSelectedModel(providerId, modelName); + + switchModel(model); + addRecentModel(model); + localStorage.setItem('GOOSE_PROVIDER', providerId); + + toast.success( + `Selected ${provider.name} provider. Starting Goose with default model: ${getDefaultModel(provider.name.toLowerCase())}.` + ); + + onSubmit?.(); + }; + + const handleAddKeys = (provider) => { + setSelectedId(provider.id); + setShowSetupModal(true); + }; + + const handleModalSubmit = async (apiKey: string) => { + if (!selectedId) return; + + const provider = providers.find((p) => p.id === selectedId)?.name; + const keyName = required_keys[provider]?.[0]; + + if (!keyName) { + console.error(`No key found for provider ${provider}`); + return; + } + + try { + if (selectedId && providers.find((p) => p.id === selectedId)?.isConfigured) { + const deleteResponse = await fetch(getApiUrl('/secrets/delete'), { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': getSecretKey(), + }, + body: JSON.stringify({ key: keyName }), + }); + + if (!deleteResponse.ok) { + const errorText = await deleteResponse.text(); + console.error('Delete response error:', errorText); + throw new Error('Failed to delete old key'); + } + } + + const storeResponse = await fetch(getApiUrl('/secrets/store'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': getSecretKey(), + }, + body: JSON.stringify({ + key: keyName, + value: apiKey.trim(), + }), + }); + + if (!storeResponse.ok) { + const errorText = await storeResponse.text(); + console.error('Store response error:', errorText); + throw new Error('Failed to store new key'); + } + + const isUpdate = selectedId && providers.find((p) => p.id === selectedId)?.isConfigured; + toast.success( + isUpdate + ? `Successfully updated API key for ${provider}` + : `Successfully added API key for ${provider}` + ); + + const updatedKeys = await getActiveProviders(); + setActiveKeys(updatedKeys); + + setShowSetupModal(false); + setSelectedId(null); + } catch (error) { + console.error('Error handling modal submit:', error); + toast.error( + `Failed to ${selectedId && providers.find((p) => p.id === selectedId)?.isConfigured ? 'update' : 'add'} API key for ${provider}` + ); + } + }; + + const handleSelect = (providerId: string) => { + setSelectedId(selectedId === providerId ? null : providerId); + }; + + // Add useEffect for Esc key handling + React.useEffect(() => { + const handleEsc = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setSelectedId(null); + } + }; + window.addEventListener('keydown', handleEsc); + return () => { + window.removeEventListener('keydown', handleEsc); + }; + }, []); + + return ( +
+
+ {selectedId && ( +
+ +
+ )} +
+ +
+ Configure your AI model providers by adding their API keys. Your keys are stored securely + and encrypted locally. You can change your provider and select specific models in the + settings. +
+ +
+ {providers.map((provider) => ( + handleSelect(provider.id)} + onConfigure={() => handleConfigure(provider)} + onAddKeys={() => handleAddKeys(provider)} + /> + ))} +
+ + {showSetupModal && selectedId && ( + p.id === selectedId)?.name} + model="Example Model" + endpoint="Example Endpoint" + onSubmit={handleModalSubmit} + onCancel={() => { + setShowSetupModal(false); + setSelectedId(null); + }} + /> + )} +
+ ); +} diff --git a/ui/desktop/src/components/ui/Tooltip.tsx b/ui/desktop/src/components/ui/Tooltip.tsx index 07628208dc..48bd871674 100644 --- a/ui/desktop/src/components/ui/Tooltip.tsx +++ b/ui/desktop/src/components/ui/Tooltip.tsx @@ -1,17 +1,28 @@ -import React from 'react'; +import * as React from 'react'; +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; -interface TooltipProps { - content: string; - children: React.ReactNode; -} +import { cn } from '../../utils'; -export function Tooltip({ content, children }: TooltipProps) { - return ( -
- {children} -
- {content} -
-
- ); -} +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/ui/desktop/src/components/welcome_screen/WelcomeModal.tsx b/ui/desktop/src/components/welcome_screen/WelcomeModal.tsx deleted file mode 100644 index 84cf893955..0000000000 --- a/ui/desktop/src/components/welcome_screen/WelcomeModal.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { ProviderSetupModal } from '../settings/ProviderSetupModal'; -import { Card } from '../ui/card'; -import { ProviderList } from '../settings/providers/old_stuff/ProvidersList'; -import { getProvidersList, Provider } from '../../utils/providerUtils'; - -export const WelcomeModal = ({ - selectedProvider, - setSelectedProvider, - onSubmit, -}: { - selectedProvider: Provider | string | null; - setSelectedProvider: React.Dispatch>; - onSubmit: (apiKey: string) => void; -}) => { - const [providers, setProviders] = useState([]); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchProviders = async () => { - try { - const providerList = await getProvidersList(); - // Filter for only "anthropic" and "openai" - const filteredProviders = providerList.filter((provider) => - ['anthropic', 'openai'].includes(provider.id) - ); - setProviders(filteredProviders); - } catch (err) { - console.error('Failed to fetch providers:', err); - setError('Unable to load providers. Please try again.'); - } - }; - - fetchProviders(); - }, []); - - return ( -
- {selectedProvider ? ( - setSelectedProvider(null)} - model={''} // placeholder - endpoint={''} // placeholder - /> - ) : ( - -

Select a Provider

- {error ? ( -

{error}

- ) : ( - - )} -
- )} -
- ); -}; diff --git a/ui/desktop/src/components/welcome_screen/WelcomeScreen.tsx b/ui/desktop/src/components/welcome_screen/WelcomeScreen.tsx new file mode 100644 index 0000000000..cd4ba8f6d7 --- /dev/null +++ b/ui/desktop/src/components/welcome_screen/WelcomeScreen.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { ProviderGrid } from '../settings/providers/ProviderGrid'; +import { ScrollArea } from '../ui/scroll-area'; +import BackButton from '../ui/BackButton'; + +interface WelcomeScreenProps { + onSubmit?: () => void; +} + +export function WelcomeScreen({ onSubmit }: WelcomeScreenProps) { + return ( +
+ {/* Add draggable title bar region */} +
+ +
+ +
+ {/* Content Area */} +
+
+
+

Choose a Provider

+
+ +
+
+
+
+
+
+ ); +}