mirror of
https://github.com/aljazceru/goose.git
synced 2026-01-07 08:24:27 +01:00
fix: welcome screen ui (#780)
This commit is contained in:
@@ -32,6 +32,19 @@ function getArticle(word: string): string {
|
||||
return 'aeiouAEIOU'.indexOf(word[0]) >= 0 ? 'an' : 'a';
|
||||
}
|
||||
|
||||
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',
|
||||
Google: 'Access Gemini and other Google AI models',
|
||||
Groq: 'Access Mixtral and other Groq-hosted models',
|
||||
Databricks: 'Access models hosted on your Databricks instance',
|
||||
OpenRouter: 'Access a variety of AI models through OpenRouter',
|
||||
Ollama: 'Run and use open-source models locally',
|
||||
};
|
||||
return descriptions[provider] || `Access ${provider} models`;
|
||||
}
|
||||
|
||||
function BaseProviderCard({
|
||||
name,
|
||||
description,
|
||||
@@ -51,8 +64,12 @@ function BaseProviderCard({
|
||||
|
||||
return (
|
||||
<div className="relative h-full p-[2px] overflow-hidden rounded-[9px] group/card bg-borderSubtle hover:bg-transparent hover:duration-300">
|
||||
<div className="absolute opacity-0 group-hover/card:opacity-100 pointer-events-none w-[260px] h-[260px] top-[-50px] left-[-30px] origin-center bg-[linear-gradient(45deg,#13BBAF,#FF4F00)] animate-[rotate_6s_linear_infinite] z-[-1]"></div>
|
||||
|
||||
{/* Glowing ring */}
|
||||
<div
|
||||
className={`absolute pointer-events-none w-[260px] h-[260px] top-[-50px] left-[-30px] origin-center bg-[linear-gradient(45deg,#13BBAF,#FF4F00)] animate-[rotate_6s_linear_infinite] z-[-1] ${
|
||||
isSelected ? 'opacity-100' : 'opacity-0 group-hover/card:opacity-100'
|
||||
}`}
|
||||
></div>
|
||||
<div
|
||||
onClick={() => isSelectable && isConfigured && onSelect?.()}
|
||||
className={`relative bg-bgApp rounded-lg
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { Providers } from './Provider';
|
||||
import { ScrollArea } from '../../ui/scroll-area';
|
||||
import BackButton from '../../ui/BackButton';
|
||||
import { ConfigureProvidersGrid } from './ConfigureProvidersGrid';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useActiveKeys } from '../api_keys/ActiveKeysContext';
|
||||
import { BaseProviderGrid } from './BaseProviderGrid';
|
||||
import { BaseProviderGrid, getProviderDescription } from './BaseProviderGrid';
|
||||
import { supported_providers, provider_aliases, required_keys } from '../models/hardcoded_stuff';
|
||||
import { getProviderDescription } from './Provider';
|
||||
import { ProviderSetupModal } from '../ProviderSetupModal';
|
||||
import { getApiUrl, getSecretKey } from '../../../config';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FaArrowLeft } from 'react-icons/fa';
|
||||
|
||||
export default function Header() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="flex items-center mb-6">
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="mr-4 p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors"
|
||||
title="Go back"
|
||||
>
|
||||
<FaArrowLeft className="text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
<h1 className="text-2xl font-semibold dark:text-white flex-1">Providers</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
import { supported_providers, required_keys, provider_aliases } from '../models/hardcoded_stuff';
|
||||
import { useActiveKeys } from '../api_keys/ActiveKeysContext';
|
||||
import { ProviderSetupModal } from '../ProviderSetupModal';
|
||||
import React from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@radix-ui/react-accordion';
|
||||
import { Check, ChevronDown, Edit2, Plus, X } from 'lucide-react';
|
||||
import { Button } from '../../ui/button';
|
||||
import { getApiUrl, getSecretKey } from '../../../config';
|
||||
import { getActiveProviders } from '../api_keys/utils';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useModel } from '../models/ModelContext';
|
||||
|
||||
function ConfirmationModal({ message, onConfirm, onCancel }) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm">
|
||||
<div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[400px] bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
|
||||
<p className="text-gray-800 dark:text-gray-200 mb-6">{message}</p>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button variant="ghost" onClick={onCancel} className="text-gray-500">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={onConfirm}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Utility Functions
|
||||
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',
|
||||
Google: 'Access Gemini and other Google AI models',
|
||||
Groq: 'Access Mixtral and other Groq-hosted models',
|
||||
Databricks: 'Access models hosted on your Databricks instance',
|
||||
OpenRouter: 'Access a variety of AI models through OpenRouter',
|
||||
Ollama: 'Run and use open-source models locally',
|
||||
};
|
||||
return descriptions[provider] || `Access ${provider} models`;
|
||||
}
|
||||
|
||||
function useProviders(activeKeys) {
|
||||
return React.useMemo(() => {
|
||||
return supported_providers.map((providerName) => {
|
||||
const alias =
|
||||
provider_aliases.find((p) => p.provider === providerName)?.alias ||
|
||||
providerName.toLowerCase();
|
||||
const requiredKeys = required_keys[providerName] || [];
|
||||
const isConfigured = activeKeys.includes(providerName);
|
||||
|
||||
return {
|
||||
id: alias,
|
||||
name: providerName,
|
||||
keyName: requiredKeys,
|
||||
isConfigured,
|
||||
description: getProviderDescription(providerName),
|
||||
};
|
||||
});
|
||||
}, [activeKeys]);
|
||||
}
|
||||
|
||||
// Reusable Components
|
||||
function ProviderStatus({ isConfigured }) {
|
||||
return isConfigured ? (
|
||||
<div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-500">
|
||||
<Check className="h-4 w-4" />
|
||||
<span>Configured</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 text-sm text-red-600 dark:text-red-500">
|
||||
<X className="h-4 w-4" />
|
||||
<span>Not Configured</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderKeyList({ keyNames, activeKeys }) {
|
||||
return keyNames.length > 0 ? (
|
||||
<div className="text-sm space-y-2">
|
||||
<span className="text-gray-500 dark:text-gray-400">Required API Keys:</span>
|
||||
{keyNames.map((key) => (
|
||||
<div key={key} className="flex items-center gap-2">
|
||||
<code className="font-mono bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">{key}</code>
|
||||
{activeKeys.includes(key) && <Check className="h-4 w-4 text-green-500" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">No API keys required</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderActions({ provider, onEdit, onDelete, onAdd }) {
|
||||
if (!provider.keyName || provider.keyName.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return provider.isConfigured ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
onClick={() => onEdit(provider)}
|
||||
className="h-9 px-4 text-sm whitespace-nowrap shrink-0
|
||||
bg-gray-800 text-white dark:bg-gray-200 dark:text-gray-900
|
||||
rounded-full shadow-md border-none
|
||||
hover:bg-gray-700 hover:text-white
|
||||
focus:outline-none focus:ring-2 focus:ring-gray-500
|
||||
dark:hover:bg-gray-300 dark:hover:text-gray-900"
|
||||
>
|
||||
<Edit2 className="h-4 w-4 mr-2" />
|
||||
Edit Keys
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
onClick={() => onDelete(provider)}
|
||||
className="h-9 px-4 text-sm whitespace-nowrap shrink-0
|
||||
rounded-full shadow-sm border-red-200 dark:border-red-800
|
||||
text-red-600 dark:text-red-500
|
||||
hover:bg-red-50 hover:text-red-700 hover:border-red-300
|
||||
dark:hover:bg-red-950/50 dark:hover:text-red-400
|
||||
focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
>
|
||||
Delete Keys
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
onClick={() => onAdd(provider)}
|
||||
className="h-9 px-4 text-sm whitespace-nowrap shrink-0
|
||||
bg-gray-800 text-white dark:bg-gray-200 dark:text-gray-900
|
||||
rounded-full shadow-md border-none
|
||||
hover:bg-gray-700 hover:text-white
|
||||
focus:outline-none focus:ring-2 focus:ring-gray-500
|
||||
dark:hover:bg-gray-300 dark:hover:text-gray-900"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Keys
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderItem({ provider, activeKeys, onEdit, onDelete, onAdd }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={provider.id}
|
||||
value={provider.id}
|
||||
className="px-6 bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700 last:border-b-0"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline py-4">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="font-semibold text-gray-900 dark:text-gray-100">{provider.name}</div>
|
||||
<ProviderStatus isConfigured={provider.isConfigured} />
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-gray-500 dark:text-gray-400 transition-transform duration-200" />
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pt-4 pb-6">
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">{provider.description}</p>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<ProviderKeyList keyNames={provider.keyName} activeKeys={activeKeys} />
|
||||
<ProviderActions
|
||||
provider={provider}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onAdd={onAdd}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
|
||||
// Main Component
|
||||
export function Providers() {
|
||||
const { activeKeys, setActiveKeys } = useActiveKeys();
|
||||
const providers = useProviders(activeKeys);
|
||||
const [selectedProvider, setSelectedProvider] = React.useState(null);
|
||||
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
||||
const [isConfirmationOpen, setIsConfirmationOpen] = React.useState(false);
|
||||
const { currentModel } = useModel();
|
||||
|
||||
const handleEdit = (provider) => {
|
||||
setSelectedProvider(provider);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleAdd = (provider) => {
|
||||
setSelectedProvider(provider);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleModalSubmit = async (apiKey) => {
|
||||
if (!selectedProvider) return;
|
||||
|
||||
const provider = selectedProvider.name;
|
||||
const keyName = required_keys[provider]?.[0]; // Get the first key, assuming one key per provider
|
||||
|
||||
if (!keyName) {
|
||||
console.error(`No key found for provider ${provider}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (selectedProvider.isConfigured) {
|
||||
// Delete existing key logic if configured
|
||||
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');
|
||||
}
|
||||
|
||||
console.log('Old key deleted successfully.');
|
||||
}
|
||||
|
||||
// Store new key logic
|
||||
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');
|
||||
}
|
||||
|
||||
console.log('Key stored successfully.');
|
||||
|
||||
// Show success toast
|
||||
toast.success(
|
||||
selectedProvider.isConfigured
|
||||
? `Successfully updated API key for ${provider}`
|
||||
: `Successfully added API key for ${provider}`
|
||||
);
|
||||
|
||||
// Update active keys
|
||||
const updatedKeys = await getActiveProviders();
|
||||
setActiveKeys(updatedKeys);
|
||||
|
||||
setIsModalOpen(false);
|
||||
} catch (error) {
|
||||
console.error('Error handling modal submit:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (provider) => {
|
||||
setSelectedProvider(provider);
|
||||
setIsConfirmationOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (!selectedProvider) return;
|
||||
|
||||
const provider = selectedProvider.name;
|
||||
const keyName = required_keys[provider]?.[0]; // Get the first key, assuming one key per provider
|
||||
|
||||
if (!keyName) {
|
||||
console.error(`No key found for provider ${provider}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if the selected provider is currently active
|
||||
if (currentModel?.provider === provider) {
|
||||
toast.error(
|
||||
`Cannot delete the API key for ${provider} because it's the provider of the current model (${currentModel.name}). Please switch to a different model first.`
|
||||
);
|
||||
setIsConfirmationOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete old key logic
|
||||
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 key');
|
||||
}
|
||||
|
||||
console.log('Key deleted successfully.');
|
||||
// Show success toast
|
||||
toast.success(`Successfully deleted API key for ${provider}`);
|
||||
|
||||
// Update active keys
|
||||
const updatedKeys = await getActiveProviders();
|
||||
setActiveKeys(updatedKeys);
|
||||
|
||||
setIsConfirmationOpen(false);
|
||||
} catch (error) {
|
||||
console.error('Error confirming delete:', error);
|
||||
// Show success toast
|
||||
toast.error(`Unable to delete API key for ${provider}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-6">
|
||||
Configure your AI model providers by adding their API keys. Your keys are stored securely
|
||||
and encrypted locally.
|
||||
</div>
|
||||
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full divide-y divide-gray-100 dark:divide-gray-700"
|
||||
>
|
||||
{providers.map((provider) => (
|
||||
<ProviderItem
|
||||
key={provider.id}
|
||||
provider={provider}
|
||||
activeKeys={activeKeys}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onAdd={handleAdd}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
{isModalOpen && selectedProvider && (
|
||||
<ProviderSetupModal
|
||||
provider={selectedProvider.name}
|
||||
model="Example Model"
|
||||
endpoint="Example Endpoint"
|
||||
onSubmit={handleModalSubmit}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isConfirmationOpen && selectedProvider && (
|
||||
<ConfirmationModal
|
||||
message={`Are you sure you want to delete the API key for ${selectedProvider.name}? This action cannot be undone.`}
|
||||
onConfirm={confirmDelete}
|
||||
onCancel={() => setIsConfirmationOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,105 +1,22 @@
|
||||
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 { Button } from '../ui/button';
|
||||
import {
|
||||
supported_providers,
|
||||
required_keys,
|
||||
provider_aliases,
|
||||
} from '../settings/models/hardcoded_stuff';
|
||||
import { useActiveKeys } from '../settings/api_keys/ActiveKeysContext';
|
||||
import { ProviderSetupModal } from '../settings/ProviderSetupModal';
|
||||
import { useModel } from '../settings/models/ModelContext';
|
||||
import { useRecentModels } from '../settings/models/RecentModels';
|
||||
import { createSelectedModel } from '../settings/models/utils';
|
||||
import { getDefaultModel } from '../settings/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 { getActiveProviders } from '../settings/api_keys/utils';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { BaseProviderGrid } from './BaseProviderGrid';
|
||||
|
||||
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 (
|
||||
<div
|
||||
onClick={() => 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' : ''}
|
||||
`}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-base font-semibold text-gray-900 dark:text-gray-100 truncate mr-2">
|
||||
{name}
|
||||
</h3>
|
||||
{isConfigured && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center justify-center w-5 h-5 rounded-full bg-green-100 dark:bg-green-900/30 shrink-0">
|
||||
<Check className="h-3 w-3 text-green-600 dark:text-green-500" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
You have {getArticle(name)} {name} API Key set in your environment
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-gray-600 dark:text-gray-400 mt-1.5 mb-3 leading-normal overflow-y-auto max-h-[48px] pr-1">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
<div className="absolute bottom-2 right-3">
|
||||
{!isConfigured && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
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"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
||||
Add Key
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { BaseProviderGrid, getProviderDescription } from '../settings/providers/BaseProviderGrid';
|
||||
|
||||
interface ProviderGridProps {
|
||||
onSubmit?: () => void;
|
||||
@@ -249,20 +166,16 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) {
|
||||
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"
|
||||
className={
|
||||
'bg-black dark:bg-white dark:hover:bg-gray-200 text-white dark:!text-black border-borderStandard hover:bg-slate text-sm whitespace-nowrap shrink-0 bg-bgSubtle text-textStandard rounded-full shadow-none border px-4 py-2'
|
||||
}
|
||||
>
|
||||
Select {providers.find((p) => p.id === selectedId)?.name}
|
||||
Let's takeoff
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
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.
|
||||
</div>
|
||||
|
||||
<BaseProviderGrid
|
||||
providers={providers}
|
||||
isSelectable={true}
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { ProviderGrid } from '../settings/providers/ProviderGrid';
|
||||
import { ProviderGrid } from './ProviderGrid';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import GooseSplashLogo from '../GooseSplashLogoGradient';
|
||||
import { Button } from '../ui/button';
|
||||
|
||||
// Extending React CSSProperties to include custom webkit property
|
||||
declare module 'react' {
|
||||
interface CSSProperties {
|
||||
WebkitAppRegion?: string; // Now TypeScript knows about WebkitAppRegion
|
||||
WebkitAppRegion?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,25 +17,58 @@ interface WelcomeScreenProps {
|
||||
|
||||
export function WelcomeScreen({ onSubmit }: WelcomeScreenProps) {
|
||||
return (
|
||||
<div className="h-screen w-full select-none">
|
||||
<div className="h-screen w-full select-none bg-white dark:bg-black">
|
||||
{/* Draggable title bar region */}
|
||||
<div className="h-[36px] w-full bg-transparent" style={{ WebkitAppRegion: 'drag' }} />
|
||||
|
||||
{/* Content area - explicitly set as non-draggable */}
|
||||
<div
|
||||
className="h-[calc(100vh-36px)] w-full bg-white dark:bg-gray-800 overflow-hidden p-2 pt-0"
|
||||
className="h-[calc(100vh-36px)] w-full overflow-hidden"
|
||||
style={{ WebkitAppRegion: 'no-drag' }}
|
||||
>
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="flex min-h-full">
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 px-16 py-8 pt-[20px]">
|
||||
<div className="max-w-3xl space-y-12">
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Choose a Provider</h1>
|
||||
</div>
|
||||
<ProviderGrid onSubmit={onSubmit} />
|
||||
</div>
|
||||
<div className="flex min-h-full flex-col justify-center px-4 py-8 md:px-16 max-w-4xl mx-auto">
|
||||
{/* Header Section */}
|
||||
<div className="mb-12 space-y-4">
|
||||
<GooseSplashLogo className="h-24 w-24 md:h-32 md:w-32" />
|
||||
<h1 className="text-4xl font-bold text-textStandard tracking-tight md:text-5xl">
|
||||
Welcome to goose
|
||||
</h1>
|
||||
<p className="text-lg text-textSubtle max-w-2xl">
|
||||
Your intelligent AI assistant for seamless productivity and creativity.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ProviderGrid */}
|
||||
<div className="w-full">
|
||||
<h2 className="text-3xl font-bold text-textStandard tracking-tight mb-2">
|
||||
Choose a Provider
|
||||
</h2>
|
||||
<p className="text-xl text-textStandard mb-4">
|
||||
Select an AI model provider to get started with goose.
|
||||
</p>
|
||||
<p className="text-sm text-textSubtle mb-8">
|
||||
Click on a provider to configure its API keys and start using goose. Your keys are
|
||||
stored securely and encrypted locally. You can change your provider and select
|
||||
specific models in the settings.
|
||||
</p>
|
||||
<ProviderGrid onSubmit={onSubmit} />
|
||||
</div>
|
||||
|
||||
{/* Get started (now less prominent) */}
|
||||
<div className="mt-12">
|
||||
<p className="text-sm text-textSubtle">
|
||||
Not sure where to start?{' '}
|
||||
<Button
|
||||
variant="link"
|
||||
className="text-indigo-500 hover:text-indigo-600 p-0 h-auto"
|
||||
onClick={() =>
|
||||
window.open('https://block.github.io/goose/v1/docs/quickstart', '_blank')
|
||||
}
|
||||
>
|
||||
Quick Start Guide
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
Reference in New Issue
Block a user