mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
feat: welcome page (#696)
This commit is contained in:
@@ -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 <WelcomeScreen onSubmit={handleSubmit} />;
|
||||
}
|
||||
|
||||
// Only render ChatLayout if not showing welcome screen
|
||||
return (
|
||||
<div>
|
||||
<ChatLayout mode={mode}>
|
||||
@@ -491,20 +465,6 @@ export default function ChatWindow() {
|
||||
setProgressMessage={setProgressMessage}
|
||||
setWorking={setWorking}
|
||||
/>
|
||||
{/*
|
||||
<WingToWing
|
||||
onExpand={toggleMode}
|
||||
progressMessage={progressMessage}
|
||||
working={working}
|
||||
/>
|
||||
*/}
|
||||
{showWelcomeModal && (
|
||||
<WelcomeModal
|
||||
selectedProvider={selectedProvider}
|
||||
setSelectedProvider={setSelectedProvider}
|
||||
onSubmit={handleModalSubmit}
|
||||
/>
|
||||
)}
|
||||
</ChatLayout>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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 = ({
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/settings/more-models" element={<MoreModelsSettings />} />
|
||||
<Route path="/settings/configure-providers" element={<ConfigureProviders />} />
|
||||
<Route path="/welcome" element={<WelcomeScreen />} />
|
||||
<Route path="*" element={<Navigate to="/chat/1" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProviderGridProps {
|
||||
onSubmit?: () => void;
|
||||
}
|
||||
|
||||
export function ProviderGrid({ onSubmit }: ProviderGridProps) {
|
||||
const { activeKeys, setActiveKeys } = useActiveKeys();
|
||||
const [selectedId, setSelectedId] = React.useState<string | null>(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 (
|
||||
<div className="space-y-4 max-w-[1400px] mx-auto">
|
||||
<div className="h-[52px]">
|
||||
{selectedId && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
onClick={() => {
|
||||
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}
|
||||
</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>
|
||||
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-3 auto-rows-fr max-w-full">
|
||||
{providers.map((provider) => (
|
||||
<ProviderCard
|
||||
key={provider.id}
|
||||
name={provider.name}
|
||||
description={provider.description}
|
||||
isConfigured={provider.isConfigured}
|
||||
isSelected={selectedId === provider.id}
|
||||
onSelect={() => handleSelect(provider.id)}
|
||||
onConfigure={() => handleConfigure(provider)}
|
||||
onAddKeys={() => handleAddKeys(provider)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showSetupModal && selectedId && (
|
||||
<ProviderSetupModal
|
||||
provider={providers.find((p) => p.id === selectedId)?.name}
|
||||
model="Example Model"
|
||||
endpoint="Example Endpoint"
|
||||
onSubmit={handleModalSubmit}
|
||||
onCancel={() => {
|
||||
setShowSetupModal(false);
|
||||
setSelectedId(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="group relative inline-block">
|
||||
{children}
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity absolute -top-6 left-1/2 -translate-x-1/2 px-2 py-1 bg-gray-900/75 backdrop-blur-sm text-white text-xs rounded pointer-events-none whitespace-nowrap">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md bg-gray-900 px-3 py-1.5 text-xs text-gray-50 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
@@ -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<React.SetStateAction<Provider | null>>;
|
||||
onSubmit: (apiKey: string) => void;
|
||||
}) => {
|
||||
const [providers, setProviders] = useState<Provider[]>([]);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm z-[9999]">
|
||||
{selectedProvider ? (
|
||||
<ProviderSetupModal
|
||||
provider={selectedProvider.name}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={() => setSelectedProvider(null)}
|
||||
model={''} // placeholder
|
||||
endpoint={''} // placeholder
|
||||
/>
|
||||
) : (
|
||||
<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-xl shadow-xl overflow-hidden p-[16px] pt-[24px]">
|
||||
<h2 className="text-2xl font-medium mb-6 dark:text-white">Select a Provider</h2>
|
||||
{error ? (
|
||||
<p className="text-center text-red-500">{error}</p>
|
||||
) : (
|
||||
<ProviderList
|
||||
providers={providers} // Use state here
|
||||
onProviderSelect={setSelectedProvider}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<div className="h-screen w-full">
|
||||
{/* Add draggable title bar region */}
|
||||
<div className="h-[36px] w-full bg-transparent window-drag" />
|
||||
|
||||
<div className="h-[calc(100vh-36px)] w-full bg-white dark:bg-gray-800 overflow-hidden p-2 pt-0">
|
||||
<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>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user