mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Enhanced onboarding page visual design (#4156)
Co-authored-by: Zane Staggs <zane@squareup.com>
This commit is contained in:
@@ -81,7 +81,7 @@ describe('OllamaSetup', () => {
|
||||
render(<OllamaSetup onSuccess={mockOnSuccess} onCancel={mockOnCancel} />);
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Use a different provider'));
|
||||
fireEvent.click(screen.getByText('Cancel'));
|
||||
});
|
||||
|
||||
expect(mockOnCancel).toHaveBeenCalled();
|
||||
@@ -156,7 +156,7 @@ describe('OllamaSetup', () => {
|
||||
render(<OllamaSetup onSuccess={mockOnSuccess} onCancel={mockOnCancel} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Ollama is running on your system/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Ollama is detected and running/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -252,7 +252,7 @@ describe('OllamaSetup', () => {
|
||||
pollCallback!({ isRunning: true, host: 'http://127.0.0.1:11434' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('✓ Ollama is running on your system')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ollama is detected and running')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '../utils/ollamaDetection';
|
||||
import { initializeSystem } from '../utils/providerUtils';
|
||||
import { toastService } from '../toasts';
|
||||
import { Ollama } from './icons';
|
||||
|
||||
interface OllamaSetupProps {
|
||||
onSuccess: () => void;
|
||||
@@ -144,7 +145,9 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
{/* Header with icon above heading - left aligned like onboarding cards */}
|
||||
<div className="text-left">
|
||||
<Ollama className="w-6 h-6 mb-3 text-text-standard" />
|
||||
<h3 className="text-lg font-semibold text-text-standard mb-2">Ollama Setup</h3>
|
||||
<p className="text-text-muted">
|
||||
Ollama lets you run AI models for free, private and locally on your computer.
|
||||
@@ -153,8 +156,8 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
|
||||
|
||||
{ollamaDetected ? (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-background-success/10 border border-border-success rounded-lg p-4">
|
||||
<p className="text-text-success text-center">✓ Ollama is running on your system</p>
|
||||
<div className="flex items-start mb-16">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-green-600 text-white rounded-full">Ollama is detected and running</span>
|
||||
</div>
|
||||
|
||||
{modelStatus === 'checking' ? (
|
||||
@@ -163,11 +166,11 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
|
||||
</div>
|
||||
) : modelStatus === 'not-available' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-background-warning/10 border border-border-warning rounded-lg p-4">
|
||||
<p className="text-text-warning text-center text-sm">
|
||||
<div className="flex items-start mb-16">
|
||||
<p className="text-text-warning text-sm">
|
||||
The {getPreferredModel()} model is not installed
|
||||
</p>
|
||||
<p className="text-text-muted text-center text-xs mt-1">
|
||||
<p className="text-text-muted text-xs mt-1">
|
||||
This model is recommended for the best experience with Goose
|
||||
</p>
|
||||
</div>
|
||||
@@ -182,12 +185,12 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
|
||||
) : modelStatus === 'downloading' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-background-info/10 border border-border-info rounded-lg p-4">
|
||||
<p className="text-text-info text-center text-sm">
|
||||
<p className="text-text-info text-sm">
|
||||
Downloading {getPreferredModel()}...
|
||||
</p>
|
||||
{downloadProgress && (
|
||||
<>
|
||||
<p className="text-text-muted text-center text-xs mt-2">
|
||||
<p className="text-text-muted text-xs mt-2">
|
||||
{downloadProgress.status}
|
||||
</p>
|
||||
{downloadProgress.total && downloadProgress.completed && (
|
||||
@@ -200,7 +203,7 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-text-muted text-center text-xs mt-1">
|
||||
<p className="text-text-muted text-xs mt-1">
|
||||
{Math.round((downloadProgress.completed / downloadProgress.total) * 100)}%
|
||||
</p>
|
||||
</div>
|
||||
@@ -221,8 +224,8 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-background-warning/10 border border-border-warning rounded-lg p-4">
|
||||
<p className="text-text-warning text-center">Ollama is not detected on your system</p>
|
||||
<div className="flex items-start mb-16">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-orange-600 text-white rounded-full">Ollama is not detected on your system</span>
|
||||
</div>
|
||||
|
||||
{isPolling ? (
|
||||
@@ -230,8 +233,8 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-t-2 border-b-2 border-textStandard"></div>
|
||||
</div>
|
||||
<p className="text-center text-text-muted text-sm">Waiting for Ollama to start...</p>
|
||||
<p className="text-center text-text-muted text-xs">
|
||||
<p className="text-text-muted text-sm">Waiting for Ollama to start...</p>
|
||||
<p className="text-text-muted text-xs">
|
||||
Once Ollama is installed and running, we'll automatically detect it.
|
||||
</p>
|
||||
</div>
|
||||
@@ -253,7 +256,7 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
|
||||
onClick={onCancel}
|
||||
className="w-full px-6 py-3 bg-transparent text-text-muted rounded-lg hover:bg-background-muted transition-colors"
|
||||
>
|
||||
Use a different provider
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,8 @@ import { initializeSystem } from '../utils/providerUtils';
|
||||
import { toastService } from '../toasts';
|
||||
import { OllamaSetup } from './OllamaSetup';
|
||||
import { checkOllamaStatus } from '../utils/ollamaDetection';
|
||||
import { Goose } from './icons/Goose';
|
||||
import { OpenRouter, Ollama } from './icons';
|
||||
|
||||
interface ProviderGuardProps {
|
||||
children: React.ReactNode;
|
||||
@@ -73,6 +75,9 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
|
||||
setOpenRouterSetupState(null);
|
||||
setShowFirstTimeSetup(false);
|
||||
setHasProvider(true);
|
||||
|
||||
// Navigate to chat after successful setup
|
||||
navigate('/', { replace: true });
|
||||
} else {
|
||||
throw new Error('Provider or model not found after OpenRouter setup');
|
||||
}
|
||||
@@ -106,8 +111,6 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
|
||||
const model = (await read('GOOSE_MODEL', false)) ?? config.GOOSE_DEFAULT_MODEL;
|
||||
|
||||
// Always check for Ollama regardless of provider status
|
||||
const ollamaStatus = await checkOllamaStatus();
|
||||
setOllamaDetected(ollamaStatus.isRunning);
|
||||
|
||||
if (provider && model) {
|
||||
console.log('ProviderGuard - Provider and model found, continuing normally');
|
||||
@@ -116,6 +119,9 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
|
||||
console.log('ProviderGuard - No provider/model configured');
|
||||
setShowFirstTimeSetup(true);
|
||||
}
|
||||
const ollamaStatus = await checkOllamaStatus();
|
||||
setOllamaDetected(ollamaStatus.isRunning);
|
||||
|
||||
} catch (error) {
|
||||
// On error, assume no provider and redirect to welcome
|
||||
console.error('Error checking provider configuration:', error);
|
||||
@@ -170,7 +176,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
|
||||
|
||||
if (showOllamaSetup) {
|
||||
return (
|
||||
<div className="h-screen w-full flex flex-col items-center justify-center bg-background-default">
|
||||
<div className="min-h-screen w-full flex flex-col items-center justify-center p-4 bg-background-default">
|
||||
<div className="max-w-md w-full mx-auto p-8">
|
||||
<div className="mb-8 text-center">
|
||||
<WelcomeGooseLogo />
|
||||
@@ -179,6 +185,8 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
|
||||
onSuccess={() => {
|
||||
setShowOllamaSetup(false);
|
||||
setHasProvider(true);
|
||||
// Navigate to chat after successful setup
|
||||
navigate('/', { replace: true });
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowOllamaSetup(false);
|
||||
@@ -192,53 +200,124 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
|
||||
|
||||
if (showFirstTimeSetup) {
|
||||
return (
|
||||
<div className="h-screen w-full flex flex-col items-center justify-center bg-background-default">
|
||||
<div className="max-w-md w-full mx-auto p-8 text-center">
|
||||
<WelcomeGooseLogo />
|
||||
<h1 className="text-2xl font-bold text-text-standard mt-8 mb-4">Welcome to Goose!</h1>
|
||||
<p className="text-text-muted mb-8">
|
||||
Let's get you set up with an AI provider to start using Goose.
|
||||
</p>
|
||||
<div className="h-screen w-full bg-background-default overflow-hidden">
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="min-h-full flex flex-col items-center justify-center p-4 py-8">
|
||||
<div className="max-w-lg w-full mx-auto p-8">
|
||||
{/* Header section - same width as buttons, left aligned */}
|
||||
<div className="text-left mb-8 sm:mb-12">
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<div className="origin-bottom-left goose-icon-animation">
|
||||
<Goose className="size-6 sm:size-8" />
|
||||
</div>
|
||||
<h1 className="text-2xl sm:text-4xl font-light text-left">
|
||||
Welcome to Goose
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-text-muted text-base sm:text-lg mt-4 sm:mt-6">
|
||||
Since it's your first time here, let's get your set you with a provider so we can make incredible work together.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={handleOpenRouterSetup}
|
||||
className="w-full px-6 py-3 bg-background-muted text-text-standard rounded-lg hover:bg-background-hover transition-colors font-medium flex items-center justify-center gap-2"
|
||||
>
|
||||
Automatic setup with OpenRouter (recommended)
|
||||
</button>
|
||||
{/* Setup options - same width container */}
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{/* Primary OpenRouter Card with subtle shimmer - wrapped for badge positioning */}
|
||||
<div className="relative">
|
||||
{/* Recommended badge - positioned relative to wrapper */}
|
||||
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-blue-600 text-white rounded-full">
|
||||
Recommended
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={handleOpenRouterSetup}
|
||||
className="relative w-full p-4 sm:p-6 bg-background-muted border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group overflow-hidden"
|
||||
>
|
||||
{/* Subtle shimmer effect */}
|
||||
<div className="absolute inset-0 -translate-x-full animate-shimmer bg-gradient-to-r from-transparent via-white/8 to-transparent"></div>
|
||||
|
||||
<div className="relative flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<OpenRouter className="w-5 h-5 sm:w-6 sm:h-6 mb-12 text-text-standard" />
|
||||
<h3 className="font-medium text-text-standard text-sm sm:text-base">
|
||||
Automatic setup with OpenRouter
|
||||
</h3>
|
||||
</div>
|
||||
<div className="text-text-muted group-hover:text-text-standard transition-colors">
|
||||
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="relative text-text-muted text-sm sm:text-base">
|
||||
Get instant access to multiple AI models including GPT-4, Claude, and more. Quick setup with just a few clicks.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowFirstTimeSetup(false);
|
||||
setShowOllamaSetup(true);
|
||||
}}
|
||||
className="w-full px-6 py-3 bg-background-muted text-text-standard rounded-lg hover:bg-background-hover transition-colors font-medium flex items-center justify-center gap-2"
|
||||
>
|
||||
{ollamaDetected ? (
|
||||
<>
|
||||
<span className="text-text-success">●</span>
|
||||
Use Ollama (auto detected)
|
||||
</>
|
||||
) : (
|
||||
'Set up Ollama (run AI locally and free)'
|
||||
{/* Ollama Card - outline style */}
|
||||
<div className="relative">
|
||||
{/* Detected badge - similar to recommended but green */}
|
||||
{ollamaDetected && (
|
||||
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-green-600 text-white rounded-full">
|
||||
Detected
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div
|
||||
onClick={() => {
|
||||
setShowFirstTimeSetup(false);
|
||||
setShowOllamaSetup(true);
|
||||
}}
|
||||
className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<Ollama className="w-5 h-5 sm:w-6 sm:h-6 mb-12 text-text-standard" />
|
||||
<h3 className="font-medium text-text-standard text-sm sm:text-base">
|
||||
Ollama
|
||||
</h3>
|
||||
</div>
|
||||
<div className="text-text-muted group-hover:text-text-standard transition-colors flex-shrink-0">
|
||||
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-text-muted text-sm sm:text-base">
|
||||
Run AI models locally on your computer. Completely free and private with no internet required.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
{/* Other providers Card - outline style */}
|
||||
<div
|
||||
onClick={() => navigate('/welcome', { replace: true })}
|
||||
className="w-full px-6 py-3 bg-background-muted text-text-standard rounded-lg hover:bg-background-hover transition-colors font-medium"
|
||||
className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group"
|
||||
>
|
||||
Configure Other Providers (advanced)
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-text-standard text-sm sm:text-base">
|
||||
Other providers
|
||||
</h3>
|
||||
</div>
|
||||
<div className="text-text-muted group-hover:text-text-standard transition-colors">
|
||||
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-text-muted text-sm sm:text-base">
|
||||
If you've already signed up for providers like Anthropic, OpenAI etc, you can enter your own keys.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-text-muted mt-6">
|
||||
OpenRouter provides instant access to multiple AI models with a simple setup.
|
||||
{ollamaDetected
|
||||
? ' Ollama is also detected on your system for running models locally.'
|
||||
: ' You can also install Ollama to run free AI models locally on your computer.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
export default function Ollama({ className = '' }) {
|
||||
return (
|
||||
<svg
|
||||
width="69"
|
||||
height="88"
|
||||
viewBox="0 0 69 88"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M65 85L62.0637 70.8077C62.0222 70.6074 62.0432 70.3992 62.1238 70.2112L64.3359 65.0497C65.7194 61.8214 65.7378 58.1708 64.387 54.9287L62.4897 50.3753C61.8577 48.8586 61.8754 47.149 62.5386 45.6457L64.1296 42.0397C66.8761 35.8141 64.3334 28.5656 58.0958 25.8464C55.8537 24.869 53.319 23.8076 50.5427 22.7131C50.1848 22.572 49.843 22.3777 49.5389 22.1421C46.6697 19.9189 44.1695 18.0613 42.1059 16.5691C38.1157 13.6838 32.8534 13.645 28.9759 16.68C27.1886 18.0789 25.0635 19.8198 22.638 21.9398C22.2177 22.3071 21.7135 22.5905 21.1813 22.7586C18.6462 23.5593 16.4068 24.2677 14.4298 24.8938C6.83415 27.2996 3.28234 35.8919 6.92194 42.9796L8.28723 45.6383C9.05963 47.1424 9.16151 48.9027 8.5678 50.4859L6.65444 55.5882C5.5855 58.4387 5.55086 61.5739 6.55656 64.4473L8.42485 69.7853C8.47432 69.9266 8.49142 70.0773 8.47488 70.2261L7 83.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="18.5" cy="40.5" r="3" fill="currentColor" />
|
||||
<circle cx="52.5" cy="40.5" r="3" fill="currentColor" />
|
||||
<path
|
||||
d="M35.5 38C42.1664 38 47 42.3128 47 47C47 51.6872 42.1664 56 35.5 56C28.8336 56 24 51.6872 24 47C24 42.3128 28.8336 38 35.5 38Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
<path
|
||||
d="M33.5 45L35.5 47M35.5 47L37.5 45M35.5 47V50"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M25 20L23.8142 14.0708C23.2738 11.369 22.3756 8.75127 21.1434 6.28682L19.6522 3.30442C18.9533 1.90667 16.9727 1.86785 16.2196 3.23714V3.23714C13.7795 7.67371 12.5 12.6549 12.5 17.7183V26"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M46.5 20.5L47.7083 14.6911C48.2867 11.9102 49.2445 9.22205 50.5549 6.70202L52.1438 3.64663C52.8504 2.28778 54.7805 2.24912 55.5409 3.57858V3.57858C58.1353 8.11412 59.5 13.2486 59.5 18.4737V26.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export default function OpenRouter({ className = '' }) {
|
||||
return (
|
||||
<svg
|
||||
width="202"
|
||||
height="171"
|
||||
viewBox="0 0 202 171"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M2.20001 85.5004C8.10001 85.5004 30.8 80.4004 42.5 73.7004C54.3 67.1004 54.3 67.1004 78.6 49.8004C109.4 28.0004 131.1 35.3004 166.8 35.3004"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M2.20001 85.5004C8.10001 85.5004 30.8 80.4004 42.5 73.7004C54.3 67.1004 54.3 67.1004 78.6 49.8004C109.4 28.0004 131.1 35.3004 166.8 35.3004"
|
||||
stroke="currentColor"
|
||||
strokeWidth="35.3"
|
||||
/>
|
||||
<path
|
||||
d="M201.3 35.5002L141 70.3002V0.700195L201.3 35.5002Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="0.4"
|
||||
/>
|
||||
<path
|
||||
d="M1 85.5C6.9 85.5 29.6 90.5 41.4 97.2C53.1 103.9 53.1 103.9 77.4 121.2C108.2 143 129.9 135.7 165.6 135.7"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M1 85.5C6.9 85.5 29.6 90.5 41.4 97.2C53.1 103.9 53.1 103.9 77.4 121.2C108.2 143 129.9 135.7 165.6 135.7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="35.3"
|
||||
/>
|
||||
<path
|
||||
d="M200.1 135.4L139.8 100.7V170.2L200.1 135.4Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="0.4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,8 @@ import { Grape } from './Grape';
|
||||
import Idea from './Idea';
|
||||
import LinkedIn from './LinkedIn';
|
||||
import More from './More';
|
||||
import OpenRouter from './OpenRouter';
|
||||
import Ollama from './Ollama';
|
||||
import Refresh from './Refresh';
|
||||
import SensitiveHidden from './SensitiveHidden';
|
||||
import SensitiveVisible from './SensitiveVisible';
|
||||
@@ -80,6 +82,8 @@ export {
|
||||
LinkedIn,
|
||||
Microphone,
|
||||
More,
|
||||
OpenRouter,
|
||||
Ollama,
|
||||
Refresh,
|
||||
SensitiveHidden,
|
||||
SensitiveVisible,
|
||||
|
||||
@@ -14,7 +14,7 @@ const GridLayout = memo(function GridLayout({ children }: { children: React.Reac
|
||||
className="grid gap-4 [&_*]:z-20 p-1"
|
||||
style={{
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 200px))',
|
||||
justifyContent: 'start',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -5,7 +5,6 @@ import ProviderGrid from './ProviderGrid';
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
import { ProviderDetails } from '../../../api/types.gen';
|
||||
import { initializeSystem } from '../../../utils/providerUtils';
|
||||
import WelcomeGooseLogo from '../../WelcomeGooseLogo';
|
||||
import { toastService } from '../../../toasts';
|
||||
|
||||
interface ProviderSettingsProps {
|
||||
@@ -97,31 +96,25 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full flex flex-col">
|
||||
<div className="h-screen w-full flex flex-col bg-background-default text-text-default">
|
||||
<ScrollArea className="flex-1 w-full">
|
||||
{isOnboarding && (
|
||||
<div className="w-full max-w-6xl mx-auto mt-16">
|
||||
<div className="group/logo flex justify-left px-4 sm:px-6 md:px-8">
|
||||
<WelcomeGooseLogo className="h-16 w-16 md:h-20 md:w-20 text-black dark:text-white" />
|
||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8 pt-12 pb-4">
|
||||
{/* Consistent header pattern with back button */}
|
||||
<div className="flex flex-col pb-8 border-b border-border-default">
|
||||
<div className="flex items-center pt-2 mb-1 no-drag">
|
||||
<BackButton onClick={onClose} />
|
||||
</div>
|
||||
<h1 className="text-4xl font-light mb-4 pt-6" data-testid="provider-selection-heading">
|
||||
{isOnboarding ? 'Other providers' : 'Provider Configuration Settings'}
|
||||
</h1>
|
||||
{isOnboarding && (
|
||||
<p className="text-sm sm:text-base text-text-muted max-w-full sm:max-w-2xl">
|
||||
Select an AI model provider to get started with goose. You'll need to use API keys
|
||||
generated by each provider which will be encrypted and stored locally. You can
|
||||
change your provider at any time in settings.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8 pt-6 pb-4">
|
||||
{/* Only show back button if not in onboarding mode */}
|
||||
{!isOnboarding && <BackButton className={'mt-[42px]'} onClick={onClose} />}
|
||||
<h1
|
||||
className="text-2xl sm:text-3xl font-medium text-textStandard mt-4"
|
||||
data-testid="provider-selection-heading"
|
||||
>
|
||||
{isOnboarding ? 'Configure your providers' : 'Provider Configuration Settings'}
|
||||
</h1>
|
||||
{isOnboarding && (
|
||||
<p className="text-sm sm:text-base text-textSubtle max-w-full sm:max-w-2xl pt-2">
|
||||
Select an AI model provider to get started with goose. You'll need to use API keys
|
||||
generated by each provider which will be encrypted and stored locally. You can change
|
||||
your provider at any time in settings.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="py-8 pt-[20px]">
|
||||
|
||||
@@ -741,3 +741,32 @@ p > code.bg-inline-code {
|
||||
[data-state='collapsed'] [data-slot='sidebar-gap'] {
|
||||
will-change: width;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Subtle back-and-forth shimmer animation for onboarding card */
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
opacity: 0.15;
|
||||
}
|
||||
50% {
|
||||
transform: translateX(100%);
|
||||
opacity: 0.05;
|
||||
}
|
||||
80% {
|
||||
opacity: 0.15;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
animation: shimmer 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -37,11 +37,11 @@ test.beforeEach(async ({ }, testInfo) => {
|
||||
if (mainWindow) {
|
||||
// Get a clean test name without the full hierarchy
|
||||
const testName = testInfo.titlePath[testInfo.titlePath.length - 1];
|
||||
|
||||
|
||||
// Get provider name if we're in a provider suite
|
||||
const providerSuite = testInfo.titlePath.find(t => t.startsWith('Provider:'));
|
||||
const providerName = providerSuite ? providerSuite.split(': ')[1] : undefined;
|
||||
|
||||
|
||||
console.log(`Setting overlay for test: "${testName}"${providerName ? ` (Provider: ${providerName})` : ''}`);
|
||||
await showTestName(mainWindow, testName, providerName);
|
||||
}
|
||||
@@ -56,9 +56,9 @@ test.afterEach(async () => {
|
||||
// Helper function to select a provider
|
||||
async function selectProvider(mainWindow: any, provider: Provider) {
|
||||
console.log(`Selecting provider: ${provider.name}`);
|
||||
|
||||
|
||||
// If we're already in the chat interface, we need to reset providers
|
||||
const chatTextarea = await mainWindow.waitForSelector('[data-testid="chat-input"]', {
|
||||
const chatTextarea = await mainWindow.waitForSelector('[data-testid="chat-input"]', {
|
||||
timeout: 2000
|
||||
}).catch(() => null);
|
||||
|
||||
@@ -76,7 +76,7 @@ async function selectProvider(mainWindow: any, provider: Provider) {
|
||||
timeout: 5000,
|
||||
state: 'visible'
|
||||
});
|
||||
|
||||
|
||||
const modelsTab = await mainWindow.waitForSelector('[data-testid="settings-models-tab"]');
|
||||
await modelsTab.click();
|
||||
|
||||
@@ -90,7 +90,7 @@ async function selectProvider(mainWindow: any, provider: Provider) {
|
||||
state: 'visible'
|
||||
});
|
||||
await resetButton.click();
|
||||
|
||||
|
||||
// Wait for the reset to complete
|
||||
await mainWindow.waitForTimeout(1000);
|
||||
}
|
||||
@@ -117,13 +117,13 @@ async function selectProvider(mainWindow: any, provider: Provider) {
|
||||
}
|
||||
|
||||
// Check if we need to click "configure other providers (advanced)" button
|
||||
const configureAdvancedButton = await mainWindow.waitForSelector('button:has-text("configure other providers (advanced)")', {
|
||||
const configureAdvancedButton = await mainWindow.waitForSelector('h3:has-text("Other providers")', {
|
||||
timeout: 3000,
|
||||
state: 'visible'
|
||||
}).catch(() => null);
|
||||
|
||||
if (configureAdvancedButton) {
|
||||
console.log('Found "configure other providers (advanced)" button, clicking it...');
|
||||
console.log('Found "configure other providers" button, clicking it...');
|
||||
await configureAdvancedButton.click();
|
||||
await mainWindow.waitForTimeout(1500);
|
||||
}
|
||||
@@ -213,7 +213,7 @@ test.describe('Goose App', () => {
|
||||
// Get the main window once for all tests
|
||||
mainWindow = await electronApp.firstWindow();
|
||||
await mainWindow.waitForLoadState('domcontentloaded');
|
||||
|
||||
|
||||
// Try to wait for networkidle, but don't fail if it times out due to MCP activity
|
||||
try {
|
||||
await mainWindow.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
@@ -307,13 +307,13 @@ test.describe('Goose App', () => {
|
||||
timeout: 5000,
|
||||
state: 'visible'
|
||||
});
|
||||
|
||||
|
||||
const appTab = await mainWindow.waitForSelector('[data-testid="settings-app-tab"]');
|
||||
await appTab.click();
|
||||
|
||||
// Wait for the theme selector to be visible
|
||||
await mainWindow.waitForTimeout(1000);
|
||||
|
||||
|
||||
// Find and click the dark mode toggle button
|
||||
const darkModeButton = await mainWindow.waitForSelector('[data-testid="dark-mode-button"]');
|
||||
const lightModeButton = await mainWindow.waitForSelector('[data-testid="light-mode-button"]');
|
||||
@@ -341,13 +341,13 @@ test.describe('Goose App', () => {
|
||||
|
||||
// check that system mode is clickable
|
||||
await systemModeButton.click();
|
||||
|
||||
|
||||
// Toggle back to light mode
|
||||
await lightModeButton.click();
|
||||
|
||||
|
||||
// Pause to show return to original state
|
||||
await mainWindow.waitForTimeout(2000);
|
||||
|
||||
|
||||
// Navigate back to home
|
||||
const homeButton = await mainWindow.waitForSelector('[data-testid="sidebar-home-button"]');
|
||||
await homeButton.click();
|
||||
@@ -364,75 +364,75 @@ test.describe('Goose App', () => {
|
||||
test.describe('Chat', () => {
|
||||
test('chat interaction', async () => {
|
||||
console.log(`Testing chat interaction with ${provider.name}...`);
|
||||
|
||||
|
||||
// Find the chat input
|
||||
const chatInput = await mainWindow.waitForSelector('[data-testid="chat-input"]');
|
||||
expect(await chatInput.isVisible()).toBe(true);
|
||||
|
||||
|
||||
// Type a message
|
||||
await chatInput.fill('Hello, can you help me with a simple task?');
|
||||
|
||||
|
||||
// Take screenshot before sending
|
||||
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-before-send.png` });
|
||||
|
||||
|
||||
// Send message
|
||||
await chatInput.press('Enter');
|
||||
|
||||
|
||||
// Wait for loading indicator to appear
|
||||
console.log('Waiting for loading indicator...');
|
||||
const loadingGoose = await mainWindow.waitForSelector('[data-testid="loading-indicator"]',
|
||||
{ timeout: 2000 });
|
||||
expect(await loadingGoose.isVisible()).toBe(true);
|
||||
|
||||
|
||||
// Take screenshot of loading state
|
||||
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-loading-state.png` });
|
||||
|
||||
|
||||
// Wait for loading indicator to disappear
|
||||
console.log('Waiting for response...');
|
||||
await mainWindow.waitForSelector('[data-testid="loading-indicator"]',
|
||||
{ state: 'hidden', timeout: 30000 });
|
||||
|
||||
|
||||
// Get the latest response
|
||||
const response = await mainWindow.locator('[data-testid="message-container"]').last();
|
||||
expect(await response.isVisible()).toBe(true);
|
||||
|
||||
|
||||
// Verify response has content
|
||||
const responseText = await response.textContent();
|
||||
expect(responseText).toBeTruthy();
|
||||
expect(responseText.length).toBeGreaterThan(0);
|
||||
|
||||
|
||||
// Take screenshot of response
|
||||
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-chat-response.png` });
|
||||
});
|
||||
|
||||
|
||||
test('verify chat history', async () => {
|
||||
console.log(`Testing chat history with ${provider.name}...`);
|
||||
|
||||
|
||||
// Find the chat input again
|
||||
const chatInput = await mainWindow.waitForSelector('[data-testid="chat-input"]');
|
||||
|
||||
|
||||
// Test message sending with a specific question
|
||||
await chatInput.fill('What is 2+2?');
|
||||
|
||||
|
||||
// Send message
|
||||
await chatInput.press('Enter');
|
||||
|
||||
|
||||
// Wait for loading indicator and response
|
||||
await mainWindow.waitForSelector('[data-testid="loading-indicator"]',
|
||||
{ state: 'hidden', timeout: 30000 });
|
||||
|
||||
|
||||
// Get the latest response
|
||||
const response = await mainWindow.locator('[data-testid="message-container"]').last();
|
||||
const responseText = await response.textContent();
|
||||
expect(responseText).toBeTruthy();
|
||||
|
||||
|
||||
// Check for message history
|
||||
const messages = await mainWindow.locator('[data-testid="message-container"]').all();
|
||||
expect(messages.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
|
||||
// Take screenshot of chat history
|
||||
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-chat-history.png` });
|
||||
|
||||
|
||||
// Test command history (up arrow)
|
||||
await chatInput.press('Control+ArrowUp');
|
||||
const inputValue = await chatInput.inputValue();
|
||||
@@ -443,7 +443,7 @@ test.describe('Goose App', () => {
|
||||
test.describe('MCP Integration', () => {
|
||||
test('running quotes MCP server integration', async () => {
|
||||
console.log(`Testing Running Quotes MCP server integration with ${provider.name}...`);
|
||||
|
||||
|
||||
// Create test-results directory if it doesn't exist
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync('test-results')) {
|
||||
@@ -460,7 +460,7 @@ test.describe('Goose App', () => {
|
||||
console.log('NetworkIdle timeout (likely due to MCP activity), continuing with test...');
|
||||
}
|
||||
await mainWindow.waitForLoadState('domcontentloaded');
|
||||
|
||||
|
||||
// Wait for React app to be ready
|
||||
await mainWindow.waitForFunction(() => {
|
||||
const root = document.getElementById('root');
|
||||
@@ -477,47 +477,47 @@ test.describe('Goose App', () => {
|
||||
state: 'visible'
|
||||
});
|
||||
await extensionsButton.click();
|
||||
|
||||
|
||||
// Wait for extensions page to load
|
||||
await mainWindow.waitForTimeout(1000);
|
||||
|
||||
|
||||
// Look for Running Quotes extension card
|
||||
console.log('Looking for existing Running Quotes extension...');
|
||||
const existingExtension = await mainWindow.$('div.flex:has-text("Running Quotes")');
|
||||
|
||||
|
||||
if (existingExtension) {
|
||||
console.log('Found existing Running Quotes extension, removing it...');
|
||||
|
||||
|
||||
// Find and click the settings gear icon next to Running Quotes
|
||||
const settingsButton = await existingExtension.$('button[aria-label="Extension settings"]');
|
||||
if (settingsButton) {
|
||||
await settingsButton.click();
|
||||
|
||||
|
||||
// Wait for modal to appear
|
||||
await mainWindow.waitForTimeout(500);
|
||||
|
||||
|
||||
// Click the Remove Extension button
|
||||
const removeButton = await mainWindow.waitForSelector('button:has-text("Remove Extension")', {
|
||||
timeout: 2000,
|
||||
state: 'visible'
|
||||
});
|
||||
await removeButton.click();
|
||||
|
||||
|
||||
// Wait for confirmation modal
|
||||
await mainWindow.waitForTimeout(500);
|
||||
|
||||
|
||||
// Click the Remove button in confirmation dialog
|
||||
const confirmButton = await mainWindow.waitForSelector('button:has-text("Remove")', {
|
||||
timeout: 2000,
|
||||
state: 'visible'
|
||||
});
|
||||
await confirmButton.click();
|
||||
|
||||
|
||||
// Wait for extension to be removed
|
||||
await mainWindow.waitForTimeout(1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Now proceed with adding the extension
|
||||
console.log('Proceeding with adding Running Quotes extension...');
|
||||
|
||||
@@ -527,11 +527,11 @@ test.describe('Goose App', () => {
|
||||
timeout: 2000,
|
||||
state: 'visible'
|
||||
});
|
||||
|
||||
|
||||
// Verify add extension button is visible
|
||||
const isAddExtensionVisible = await addExtensionButton.isVisible();
|
||||
console.log('Add custom extension button visible:', isAddExtensionVisible);
|
||||
|
||||
|
||||
await addExtensionButton.click();
|
||||
console.log('Clicked Add custom extension');
|
||||
|
||||
@@ -541,21 +541,21 @@ test.describe('Goose App', () => {
|
||||
|
||||
// Fill the form
|
||||
console.log('Filling form fields...');
|
||||
|
||||
|
||||
// Fill Extension Name
|
||||
const nameInput = await mainWindow.waitForSelector('input[placeholder="Enter extension name..."]', {
|
||||
timeout: 2000,
|
||||
state: 'visible'
|
||||
});
|
||||
await nameInput.fill('Running Quotes');
|
||||
|
||||
|
||||
// Fill Description
|
||||
const descriptionInput = await mainWindow.waitForSelector('input[placeholder="Optional description..."]', {
|
||||
timeout: 2000,
|
||||
state: 'visible'
|
||||
});
|
||||
await descriptionInput.fill('Inspirational running quotes MCP server');
|
||||
|
||||
|
||||
// Fill Command
|
||||
const mcpScriptPath = join(__dirname, 'basic-mcp.ts');
|
||||
const commandInput = await mainWindow.waitForSelector('input[placeholder="e.g. npx -y @modelcontextprotocol/my-extension <filepath>"]', {
|
||||
@@ -576,50 +576,50 @@ test.describe('Goose App', () => {
|
||||
timeout: 2000,
|
||||
state: 'visible'
|
||||
});
|
||||
|
||||
|
||||
// Verify button is visible
|
||||
const isModalAddButtonVisible = await modalAddButton.isVisible();
|
||||
console.log('Add Extension button visible:', isModalAddButtonVisible);
|
||||
|
||||
// Click the button
|
||||
await modalAddButton.click();
|
||||
|
||||
|
||||
console.log('Clicked Add Extension button');
|
||||
|
||||
// Wait for the Running Quotes extension to appear in the list
|
||||
console.log('Waiting for Running Quotes extension to appear...');
|
||||
try {
|
||||
const extensionCard = await mainWindow.waitForSelector(
|
||||
'div.flex:has-text("Running Quotes")',
|
||||
'div.flex:has-text("Running Quotes")',
|
||||
{
|
||||
timeout: 30000,
|
||||
state: 'visible'
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Verify the extension is enabled
|
||||
await mainWindow.waitForTimeout(1000);
|
||||
const toggleButton = await extensionCard.$('button[role="switch"][data-state="checked"]');
|
||||
const isEnabled = !!toggleButton;
|
||||
console.log('Extension enabled:', isEnabled);
|
||||
|
||||
|
||||
if (!isEnabled) {
|
||||
throw new Error('Running Quotes extension was added but not enabled');
|
||||
}
|
||||
|
||||
|
||||
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-extension-added.png` });
|
||||
console.log('Extension added successfully');
|
||||
} catch (error) {
|
||||
console.error('Error verifying extension:', error);
|
||||
|
||||
|
||||
// Get any error messages that might be visible
|
||||
const errorElements = await mainWindow.$$eval('.text-red-500, .text-error',
|
||||
const errorElements = await mainWindow.$$eval('.text-red-500, .text-error',
|
||||
elements => elements.map(el => el.textContent)
|
||||
);
|
||||
if (errorElements.length > 0) {
|
||||
console.log('Found error messages:', errorElements);
|
||||
}
|
||||
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -631,36 +631,36 @@ test.describe('Goose App', () => {
|
||||
} catch (error) {
|
||||
// Take error screenshot and log details
|
||||
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-error.png` });
|
||||
|
||||
|
||||
// Get page content
|
||||
const pageContent = await mainWindow.evaluate(() => document.body.innerHTML);
|
||||
console.log('Page content at error:', pageContent);
|
||||
|
||||
|
||||
console.error('Test failed:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('test running quotes functionality', async () => {
|
||||
console.log(`Testing running quotes functionality with ${provider.name}...`);
|
||||
|
||||
|
||||
// Find the chat input
|
||||
const chatInput = await mainWindow.waitForSelector('[data-testid="chat-input"]');
|
||||
expect(await chatInput.isVisible()).toBe(true);
|
||||
|
||||
|
||||
// Type a message requesting a running quote
|
||||
await chatInput.fill('Can you give me an inspirational running quote using the runningQuotes tool?');
|
||||
|
||||
|
||||
// Take screenshot before sending
|
||||
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-before-quote-request.png` });
|
||||
|
||||
|
||||
// Send message
|
||||
await chatInput.press('Enter');
|
||||
|
||||
// Get the latest response
|
||||
const response = await mainWindow.waitForSelector('.goose-message-tool', { timeout: 5000 });
|
||||
expect(await response.isVisible()).toBe(true);
|
||||
|
||||
|
||||
// Click the Output dropdown to reveal the actual quote
|
||||
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-quote-response-debug.png` });
|
||||
|
||||
@@ -668,12 +668,12 @@ test.describe('Goose App', () => {
|
||||
const outputContent = await mainWindow.waitForSelector('.whitespace-pre-wrap', { timeout: 5000 });
|
||||
const outputText = await outputContent.textContent();
|
||||
console.log('Output text:', outputText);
|
||||
|
||||
|
||||
// Take screenshot of expanded response
|
||||
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-quote-response.png` });
|
||||
|
||||
|
||||
// Check if the output contains one of our known quotes
|
||||
const containsKnownQuote = runningQuotes.some(({ quote, author }) =>
|
||||
const containsKnownQuote = runningQuotes.some(({ quote, author }) =>
|
||||
outputText.includes(`"${quote}" - ${author}`)
|
||||
);
|
||||
expect(containsKnownQuote).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user