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 && (
+
{
+ e.stopPropagation();
+ onAddKeys();
+ }}
+ className="rounded-full h-7 px-3 min-w-[90px] bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100 text-xs"
+ >
+
+ Add Key
+
+ )}
+
+
+ );
+}
+
+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 && (
+
+ {
+ const provider = providers.find((p) => p.id === selectedId);
+ if (provider) handleConfigure(provider);
+ }}
+ className="rounded-full px-6 py-2 min-w-[160px] bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white dark:text-white text-sm font-medium shadow-md hover:shadow-lg transition-all"
+ >
+ Select {providers.find((p) => p.id === selectedId)?.name}
+
+
+ )}
+
+
+
+ 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
+
+
+
+
+
+
+
+
+ );
+}