Skip to content

Commit

Permalink
[setup] UX simplification for adding API keys on app startup
Browse files Browse the repository at this point in the history
  • Loading branch information
alexhancock authored and baxen committed Jan 15, 2025
1 parent e412e99 commit 52ecf7c
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 309 deletions.
158 changes: 92 additions & 66 deletions ui/desktop/src/ChatWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { Message, useChat } from './ai-sdk-fork/useChat';

import { Route, Routes, Navigate } from 'react-router-dom';
import { getApiUrl, getSecretKey } from './config';
import { ApiKeyWarning } from './components/ApiKeyWarning';
import BottomMenu from './components/BottomMenu';
import FlappyGoose from './components/FlappyGoose';
import GooseMessage from './components/GooseMessage';
Expand All @@ -15,19 +13,21 @@ import Splash from './components/Splash';
import { Card } from './components/ui/card';
import { ScrollArea } from './components/ui/scroll-area';
import UserMessage from './components/UserMessage';
import { WelcomeScreen } from './components/WelcomeScreen';
import WingToWing, { Working } from './components/WingToWing';
import { askAi } from './utils/askAI';
import { NewWelcomeScreen } from './components/setup/NewWelcomeScreen';
import mockKeychain from './services/mockKeychain';
import { ProviderSetupModal } from './components/ProviderSetupModal';
import {
providers,
ProviderOption,
OPENAI_ENDPOINT_PLACEHOLDER,
ANTHROPIC_ENDPOINT_PLACEHOLDER,
OPENAI_DEFAULT_MODEL,
ANTHROPIC_DEFAULT_MODEL
} from './utils/providerUtils';

const CURRENT_VERSION = '0.0.0';
export const PROVIDER_API_KEY = "GOOSE_PROVIDER__API_KEY" // the key to look for to make sure user has previously set an API key

// Get the last version from localStorage
const getLastSeenVersion = () => localStorage.getItem('lastSeenVersion');
const setLastSeenVersion = (version: string) => localStorage.setItem('lastSeenVersion', version);

declare global {
interface Window {
electron: {
Expand Down Expand Up @@ -132,7 +132,6 @@ function ChatContent({
const timeSinceLastInteraction = Date.now() - lastInteractionTime;
window.electron.logInfo("last interaction:" + lastInteractionTime);
if (timeSinceLastInteraction > 60000) { // 60000ms = 1 minute

window.electron.showNotification({title: 'Goose finished the task.', body: 'Click here to expand.'});
}
},
Expand Down Expand Up @@ -173,19 +172,12 @@ function ChatContent({
// Single effect to handle all scrolling
useEffect(() => {
if (isLoading || messages.length > 0 || working === Working.Working) {
// Initial scroll
scrollToBottom(isLoading || working === Working.Working ? 'instant' : 'smooth');

// // Additional scrolls to catch dynamic content
// [100, 300, 500].forEach(delay => {
// setTimeout(() => scrollToBottom('smooth'), delay);
// });
}
}, [messages, isLoading, working]);

// Handle submit
const handleSubmit = (e: React.FormEvent) => {
// Start power save blocker when sending a message
window.electron.startPowerSaveBlocker();
const customEvent = e as CustomEvent;
const content = customEvent.detail?.value || '';
Expand All @@ -195,7 +187,6 @@ function ChatContent({
role: 'user',
content: content,
});
// Immediate scroll on submit
scrollToBottom('instant');
}
};
Expand All @@ -206,7 +197,7 @@ function ChatContent({

const onStopGoose = () => {
stop();
setLastInteractionTime(Date.now()); // Update last interaction time
setLastInteractionTime(Date.now());
window.electron.stopPowerSaveBlocker();

const lastMessage: Message = messages[messages.length - 1];
Expand All @@ -222,7 +213,7 @@ function ChatContent({
setMessages([]);
}
} else if (lastMessage.role === 'assistant' && lastMessage.toolInvocations !== undefined) {
// Add messaging about interrupted ongoing tool invocations.
// Add messaging about interrupted ongoing tool invocations
const newLastMessage: Message = {
...lastMessage,
toolInvocations: lastMessage.toolInvocations.map((invocation) => {
Expand All @@ -231,16 +222,12 @@ function ChatContent({
...invocation,
result: [
{
"audience": [
"user"
],
"audience": ["user"],
"text": "Interrupted.\n",
"type": "text"
},
{
"audience": [
"assistant"
],
"audience": ["assistant"],
"text": "Interrupted by the user to make a correction.\n",
"type": "text"
}
Expand Down Expand Up @@ -343,9 +330,6 @@ function ChatContent({
// Function to send the system configuration to the server
const addSystemConfig = async (system: string) => {
console.log("calling add system")
// Get the app instance from electron
const app = window.electron.app;

const systemConfig = {
type: "Stdio",
cmd: await window.electron.getBinaryPath('goosed'),
Expand Down Expand Up @@ -418,6 +402,8 @@ export default function ChatWindow() {
);
const [working, setWorking] = useState<Working>(Working.Idle);
const [progressMessage, setProgressMessage] = useState<string>('');
const [selectedProvider, setSelectedProvider] = useState<ProviderOption | null>(null);
const [showWelcomeModal, setShowWelcomeModal] = useState(true);

// Add this useEffect to track changes and update welcome state
const toggleMode = () => {
Expand All @@ -433,50 +419,90 @@ export default function ChatWindow() {

window.electron.logInfo('ChatWindow loaded');

useEffect(() => {
// Check if we already have a provider set
const storedProvider = localStorage.getItem("GOOSE_PROVIDER");
const hasApiKey = window.appConfig.get(PROVIDER_API_KEY);

if (storedProvider && hasApiKey) {
setShowWelcomeModal(false);
} else {
setShowWelcomeModal(true);
}
}, []);

// env.GOOSE_PROVIDER || localstorage.getItem("GOOSE_PROVIDER") -> none
// if it is set we assume the keys are available somewhere (keychain or env)
// if it is not set, we run the login page
console.log("GOOSE_PROVIDER via env:", window.electron.getConfig().GOOSE_PROVIDER);
console.log("GOOSE_PROVIDER via storage:", localStorage.getItem("GOOSE_PROVIDER"));
let goose_provider = window.electron.getConfig().GOOSE_PROVIDER || localStorage.getItem("GOOSE_PROVIDER");
const handleModalSubmit = async (apiKey: string) => {
try {
const trimmedKey = apiKey.trim();
await mockKeychain.setKey(PROVIDER_API_KEY, trimmedKey);

if (selectedProvider) {
localStorage.setItem("GOOSE_PROVIDER", selectedProvider.name);
setShowWelcomeModal(false);
}
} catch (error) {
console.error('Failed to store API key:', error);
}
};

return (
<div className="relative w-screen h-screen overflow-hidden dark:bg-dark-window-gradient bg-window-gradient flex flex-col">
<div className="titlebar-drag-region" />
{goose_provider === null ? (
<div className="w-full h-full">
<NewWelcomeScreen className="w-full h-full" />
{window.electron?.logInfo?.('DEBUG: Rendering NewWelcomeScreen')}
</div>
) : (
<>
{window.electron?.logInfo?.('DEBUG: Rendering main chat interface')}
<div style={{ display: mode === 'expanded' ? 'block' : 'none' }}>
<Routes>
<Route
path="/chat/:id"
element={
<ChatContent
key={selectedChatId}
chats={chats}
setChats={setChats}
selectedChatId={selectedChatId}
setSelectedChatId={setSelectedChatId}
initialQuery={initialQuery}
setProgressMessage={setProgressMessage}
setWorking={setWorking}
/>
}
<div style={{ display: mode === 'expanded' ? 'block' : 'none' }}>
<Routes>
<Route
path="/chat/:id"
element={
<ChatContent
key={selectedChatId}
chats={chats}
setChats={setChats}
selectedChatId={selectedChatId}
setSelectedChatId={setSelectedChatId}
initialQuery={null}
setProgressMessage={setProgressMessage}
setWorking={setWorking}
/>
<Route path="/settings" element={<Settings />} />
<Route path="*" element={<Navigate to="/chat/1" replace />} />
</Routes>
</div>
}
/>
<Route path="/settings" element={<Settings />} />
<Route path="*" element={<Navigate to="/chat/1" replace />} />
</Routes>
</div>

<WingToWing onExpand={toggleMode} progressMessage={progressMessage} working={working} />
</>
<WingToWing onExpand={toggleMode} progressMessage={progressMessage} working={working} />

{showWelcomeModal && (
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm z-[9999]">
{selectedProvider ? (
<ProviderSetupModal
provider={selectedProvider.name}
model={selectedProvider.id === 'openai' ? OPENAI_DEFAULT_MODEL : ANTHROPIC_DEFAULT_MODEL}
endpoint={selectedProvider.id === 'openai' ? OPENAI_ENDPOINT_PLACEHOLDER : ANTHROPIC_ENDPOINT_PLACEHOLDER}
onSubmit={handleModalSubmit}
onCancel={() => {
setSelectedProvider(null);
}}
/>
) : (
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[440px] bg-white dark:bg-gray-800 rounded-[32px] shadow-xl overflow-hidden p-8">
<h2 className="text-2xl font-semibold text-center mb-6 dark:text-white">Select a Provider</h2>
<div className="grid grid-cols-1 gap-4">
{providers.map((provider) => (
<button
key={provider.id}
onClick={() => setSelectedProvider(provider)}
className="p-6 border rounded-lg hover:border-blue-500 transition-colors text-left dark:border-gray-700 dark:hover:border-blue-400"
>
<h3 className="text-lg font-medium mb-2 dark:text-gray-200">{provider.name}</h3>
<p className="text-gray-600 dark:text-gray-400">{provider.description}</p>
</button>
))}
</div>
</Card>
)}
</div>
)}
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import React from 'react';
import { Card } from '../ui/card';
import { Card } from './ui/card';
import { Lock } from 'lucide-react'
import { Input } from "../ui/input"
import { Button } from "../ui/button"
import UnionIcon from '../../images/[email protected]';
import { Input } from "./ui/input"
import { Button } from "./ui/button"
import UnionIcon from '../images/[email protected]';


interface AddModelModalProps {
interface ProviderSetupModalProps {
provider: string
model: string
endpoint: string
onSubmit: (apiKey: string) => void
onCancel: () => void
}

export function AddModelModal({ provider, model, endpoint, onSubmit, onCancel }: AddModelModalProps) {
export function ProviderSetupModal({ provider, model, endpoint, onSubmit, onCancel }: ProviderSetupModalProps) {
const [apiKey, setApiKey] = React.useState("")

const headerText = `Add ${provider} API Key`
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onSubmit(apiKey)
Expand All @@ -36,46 +35,24 @@ export function AddModelModal({ provider, model, endpoint, onSubmit, onCancel }:
className="w-32 h-32"
/>
</div>
<h2 className="text-2xl font-semibold text-gray-900">Add model</h2>
<p className="text-gray-500 text-lg">
Add your model details to integrate into goose
</p>
<h2 className="text-2xl font-semibold text-gray-900">{headerText}</h2>
</div>

{/* Form */}
<form onSubmit={handleSubmit} className="space-y-8">
<div className="space-y-5">
<div>
<Input
type="text"
value={endpoint}
disabled
placeholder="Endpoint"
className="w-full h-14 px-6 rounded-2xl border border-gray-200/75 bg-white text-lg placeholder:text-gray-400"
/>
</div>
<div>
<Input
type="text"
value={model}
onChange={(e) => setModel(e.target.value)}
placeholder="Model"
className="w-full h-14 px-6 rounded-2xl border border-gray-200/75 bg-white text-lg placeholder:text-gray-400"
required
/>
</div>
<div>
<Input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="API key"
placeholder={`${provider} API key`}
className="w-full h-14 px-6 rounded-2xl border border-gray-200/75 bg-white text-lg placeholder:text-gray-400"
required
/>
<div className="flex items-center gap-1.5 mt-3 text-gray-500">
<Lock className="w-4 h-4" />
<span className="text-[15px]">Keys are stored in a secure .env file</span>
<span className="text-[15px]">{`Your API key will be stored securely in the keychain and used only for making requests to ${provider}`}</span>
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit 52ecf7c

Please sign in to comment.