improve composio tools ux

This commit is contained in:
Ramnique Singh 2025-07-10 16:26:37 +05:30
parent 2ee4f37464
commit 3063c9fea9
3 changed files with 268 additions and 273 deletions

View file

@ -158,6 +158,16 @@ export function Composio() {
toolkit.meta.description.toLowerCase().includes(searchLower) || toolkit.meta.description.toLowerCase().includes(searchLower) ||
toolkit.slug.toLowerCase().includes(searchLower) toolkit.slug.toLowerCase().includes(searchLower)
); );
}).sort((a, b) => {
// Sort by actual connection status first (only connected tools, not no-auth)
const aConnected = !a.no_auth && projectConfig?.composioConnectedAccounts?.[a.slug]?.status === 'ACTIVE';
const bConnected = !b.no_auth && projectConfig?.composioConnectedAccounts?.[b.slug]?.status === 'ACTIVE';
if (aConnected && !bConnected) return -1;
if (!aConnected && bConnected) return 1;
// If both have same connection status, maintain original order (don't sort alphabetically)
return 0;
}); });
if (loading) { if (loading) {
@ -191,14 +201,6 @@ export function Composio() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-lg p-4">
<div className="flex gap-3">
<div className="shrink-0">
<Info className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
</div>
</div>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex-1 flex items-center gap-4"> <div className="flex-1 flex items-center gap-4">
@ -276,6 +278,8 @@ export function Composio() {
onClose={handleCloseToolsPanel} onClose={handleCloseToolsPanel}
projectConfig={projectConfig} projectConfig={projectConfig}
onUpdateToolsSelection={handleUpdateToolsSelection} onUpdateToolsSelection={handleUpdateToolsSelection}
onProjectConfigUpdate={handleProjectConfigUpdate}
onRemoveToolkitTools={handleRemoveToolkitTools}
isSaving={savingTools} isSaving={savingTools}
/> />
</div> </div>

View file

@ -2,15 +2,15 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { PictureImg } from '@/components/ui/picture-img'; import { PictureImg } from '@/components/ui/picture-img';
import { Checkbox } from '@heroui/react'; import { Button, Checkbox } from '@heroui/react';
import { ChevronLeft, ChevronRight } from 'lucide-react'; import { ChevronLeft, ChevronRight, LinkIcon, Loader2, UnlinkIcon } from 'lucide-react';
import { listTools } from '@/app/actions/composio_actions'; import { listTools, deleteConnectedAccount } from '@/app/actions/composio_actions';
import { z } from 'zod'; import { z } from 'zod';
import { ZTool, ZListResponse } from '@/app/lib/composio/composio'; import { ZTool, ZListResponse } from '@/app/lib/composio/composio';
import { SlidePanel } from '@/components/ui/slide-panel'; import { SlidePanel } from '@/components/ui/slide-panel';
import { Project } from '@/app/lib/types/project_types'; import { Project } from '@/app/lib/types/project_types';
import { ToolkitAuthModal } from './ToolkitAuthModal';
type ToolType = z.infer<typeof ZTool>; type ToolType = z.infer<typeof ZTool>;
type ToolListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>; type ToolListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>;
@ -29,6 +29,8 @@ interface ComposioToolsPanelProps {
onClose: () => void; onClose: () => void;
projectConfig: ProjectType | null; projectConfig: ProjectType | null;
onUpdateToolsSelection: (selectedToolObjects: ToolType[]) => void; onUpdateToolsSelection: (selectedToolObjects: ToolType[]) => void;
onProjectConfigUpdate: () => void;
onRemoveToolkitTools: (toolkitSlug: string) => void;
isSaving: boolean; isSaving: boolean;
} }
@ -38,6 +40,8 @@ export function ComposioToolsPanel({
onClose, onClose,
projectConfig, projectConfig,
onUpdateToolsSelection, onUpdateToolsSelection,
onProjectConfigUpdate,
onRemoveToolkitTools,
isSaving isSaving
}: ComposioToolsPanelProps) { }: ComposioToolsPanelProps) {
const params = useParams(); const params = useParams();
@ -51,6 +55,8 @@ export function ComposioToolsPanel({
const [cursorHistory, setCursorHistory] = useState<string[]>([]); const [cursorHistory, setCursorHistory] = useState<string[]>([]);
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set()); const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const [showAuthModal, setShowAuthModal] = useState(false);
const [isProcessingAuth, setIsProcessingAuth] = useState(false);
const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => { const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => {
try { try {
@ -117,6 +123,34 @@ export function ComposioToolsPanel({
setHasChanges(false); setHasChanges(false);
}, [onUpdateToolsSelection, selectedTools, tools]); }, [onUpdateToolsSelection, selectedTools, tools]);
const handleConnect = useCallback(() => {
setShowAuthModal(true);
}, []);
const handleDisconnect = useCallback(async () => {
if (!toolkit) return;
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.id;
setIsProcessingAuth(true);
try {
if (connectedAccountId) {
await deleteConnectedAccount(projectId, toolkit.slug, connectedAccountId);
onProjectConfigUpdate();
onRemoveToolkitTools(toolkit.slug);
}
} catch (err: any) {
console.error('Disconnect failed:', err);
} finally {
setIsProcessingAuth(false);
}
}, [projectId, toolkit, projectConfig, onProjectConfigUpdate, onRemoveToolkitTools]);
const handleAuthComplete = useCallback(() => {
setShowAuthModal(false);
onProjectConfigUpdate();
}, [onProjectConfigUpdate]);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
setTools([]); setTools([]);
setSelectedTools(new Set()); setSelectedTools(new Set());
@ -151,119 +185,170 @@ export function ComposioToolsPanel({
const isToolkitConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE'; const isToolkitConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
return ( return (
<SlidePanel <>
isOpen={isOpen} <SlidePanel
onClose={handleClose} isOpen={isOpen}
title={ onClose={handleClose}
<div className="flex items-center gap-3"> title={
{toolkit.meta.logo && ( <div className="flex items-center gap-3">
<PictureImg {toolkit.meta.logo && (
src={toolkit.meta.logo} <PictureImg
alt={`${toolkit.name} logo`} src={toolkit.meta.logo}
width={24} alt={`${toolkit.name} logo`}
height={24} width={24}
className="rounded-md object-cover" height={24}
/> className="rounded-md object-cover"
)} />
<span>{toolkit.name}</span> )}
</div> <span>{toolkit.name}</span>
}
>
<div className="flex flex-col h-full">
{/* Header */}
<div className="mb-6">
<div className="flex items-center justify-between">
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Available Tools</h4>
<div className="flex items-center gap-2">
{!isToolkitConnected && !toolkit.no_auth && (
<div className="text-sm text-orange-600 dark:text-orange-400 px-3 py-1 rounded-full bg-orange-50 dark:bg-orange-900/20">
Toolkit not connected
</div>
)}
{hasChanges && (
<Button
variant="primary"
size="sm"
onClick={handleSaveTools}
disabled={isSaving || !isToolkitConnected}
>
{isSaving ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-2 border-b-transparent border-white mr-2" />
Saving...
</>
) : (
'Save Changes'
)}
</Button>
)}
</div>
</div> </div>
</div> }
>
{/* Scrollable Tools List */} <div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto"> {/* Connection Status Banner */}
{toolsLoading ? ( {!toolkit.no_auth && (
<div className="text-center py-8"> <div className={`mb-6 p-4 rounded-lg border-2 ${
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div> isToolkitConnected
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading tools...</p> ? 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800'
</div> : 'bg-orange-50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800'
) : ( }`}>
<div className="space-y-4"> <div className="flex items-center justify-between">
{tools.map((tool) => ( <div className="flex items-center gap-3">
<div key={tool.slug} className={`group p-4 rounded-lg transition-all duration-200 border border-transparent ${ <div className={`w-3 h-3 rounded-full ${
isToolkitConnected isToolkitConnected ? 'bg-emerald-500' : 'bg-orange-500'
? 'bg-gray-50/50 dark:bg-gray-800/50 hover:bg-gray-100/50 dark:hover:bg-gray-700/50 hover:border-gray-200 dark:hover:border-gray-600' }`}></div>
: 'bg-gray-100/50 dark:bg-gray-900/50 opacity-60' <div>
}`}> <h3 className={`font-semibold text-sm ${
<div className="flex items-start gap-3"> isToolkitConnected
<Checkbox ? 'text-emerald-800 dark:text-emerald-200'
isSelected={selectedTools.has(tool.slug)} : 'text-orange-800 dark:text-orange-200'
onValueChange={(selected) => handleToolSelectionChange(tool.slug, selected)} }`}>
size="sm" {isToolkitConnected ? 'Toolkit Connected' : 'Authentication Required'}
isDisabled={!isToolkitConnected} </h3>
/> <p className={`text-xs mt-0.5 ${
<div className="flex-1"> isToolkitConnected
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1"> ? 'text-emerald-700 dark:text-emerald-300'
{tool.name} : 'text-orange-700 dark:text-orange-300'
</h4> }`}>
<p className="text-sm text-gray-500 dark:text-gray-400"> {isToolkitConnected
{tool.description} ? 'You can select and use tools from this toolkit'
</p> : 'Connect your account to access and use tools'
</div> }
</p>
</div> </div>
</div> </div>
))} <Button
variant="solid"
size="sm"
onPress={isToolkitConnected ? handleDisconnect : handleConnect}
disabled={isProcessingAuth}
color={isToolkitConnected ? "danger" : "primary"}
isLoading={isProcessingAuth}
startContent={isToolkitConnected ? <UnlinkIcon className="h-4 w-4" /> : <LinkIcon className="h-4 w-4" />}
>
{isToolkitConnected ? 'Disconnect' : 'Connect Now'}
</Button>
</div>
</div> </div>
)} )}
</div>
{/* Fixed Pagination Controls */} {/* Header */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4"> <div className="mb-6">
<div className="flex items-center justify-end"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Available Tools</h4>
<Button <div className="flex items-center gap-2">
variant="secondary" {hasChanges && (
size="sm" <Button
onClick={handlePreviousPage} variant="solid"
disabled={cursorHistory.length === 0 || toolsLoading} size="sm"
> color="primary"
<ChevronLeft className="h-4 w-4 mr-1" /> onPress={handleSaveTools}
Previous disabled={isSaving || !isToolkitConnected}
</Button> isLoading={isSaving}
<Button >
variant="secondary" Save Changes
size="sm" </Button>
onClick={handleNextPage} )}
disabled={!nextCursor || toolsLoading} </div>
> </div>
Next </div>
<ChevronRight className="h-4 w-4 ml-1" />
</Button> {/* Scrollable Tools List */}
<div className="flex-1 overflow-y-auto">
{toolsLoading ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div>
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading tools...</p>
</div>
) : (
<div className="space-y-4">
{tools.map((tool) => (
<div key={tool.slug} className={`group p-4 rounded-lg transition-all duration-200 border border-transparent ${
isToolkitConnected
? 'bg-gray-50/50 dark:bg-gray-800/50 hover:bg-gray-100/50 dark:hover:bg-gray-700/50 hover:border-gray-200 dark:hover:border-gray-600'
: 'bg-gray-100/50 dark:bg-gray-900/50 opacity-60'
}`}>
<div className="flex items-start gap-3">
<Checkbox
isSelected={selectedTools.has(tool.slug)}
onValueChange={(selected) => handleToolSelectionChange(tool.slug, selected)}
size="sm"
isDisabled={!isToolkitConnected}
/>
<div className="flex-1">
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
{tool.name}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
{tool.description}
</p>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Fixed Pagination Controls */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
<div className="flex items-center justify-end">
<div className="flex items-center gap-2">
<Button
variant="bordered"
size="sm"
onClick={handlePreviousPage}
disabled={cursorHistory.length === 0 || toolsLoading}
>
<ChevronLeft className="h-4 w-4 mr-1" />
Previous
</Button>
<Button
variant="bordered"
size="sm"
onClick={handleNextPage}
disabled={!nextCursor || toolsLoading}
>
Next
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </SlidePanel>
</SlidePanel>
{/* Auth Modal */}
{toolkit && (
<ToolkitAuthModal
key={toolkit.slug}
isOpen={showAuthModal}
onClose={() => setShowAuthModal(false)}
toolkitSlug={toolkit.slug}
projectId={projectId}
onComplete={handleAuthComplete}
/>
)}
</>
); );
} }

View file

@ -1,29 +1,28 @@
'use client'; 'use client';
import { useState, useCallback } from 'react'; import { useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { PictureImg } from '@/components/ui/picture-img'; import { PictureImg } from '@/components/ui/picture-img';
import { Wrench } from 'lucide-react';
import clsx from 'clsx'; import clsx from 'clsx';
import { Spinner } from '@heroui/react';
import { deleteConnectedAccount } from '@/app/actions/composio_actions';
import { z } from 'zod'; import { z } from 'zod';
import { ZToolkit } from '@/app/lib/composio/composio'; import { ZToolkit } from '@/app/lib/composio/composio';
import { Project } from '@/app/lib/types/project_types'; import { Project } from '@/app/lib/types/project_types';
import { ToolkitAuthModal } from './ToolkitAuthModal'; import { Chip } from '@heroui/react';
import { LinkIcon } from 'lucide-react';
type ToolkitType = z.infer<typeof ZToolkit>; type ToolkitType = z.infer<typeof ZToolkit>;
type ProjectType = z.infer<typeof Project>; type ProjectType = z.infer<typeof Project>;
const toolkitCardStyles = { const toolkitCardStyles = {
base: clsx( base: clsx(
"group p-6 rounded-xl transition-all duration-200", "group p-6 rounded-xl transition-all duration-200 cursor-pointer",
"bg-white dark:bg-gray-900 shadow-sm dark:shadow-none", "bg-white dark:bg-gray-900",
"border-2 border-gray-200/80 dark:border-gray-700/80", "border border-gray-200 dark:border-gray-700",
"hover:shadow-md dark:hover:shadow-none", "shadow-md dark:shadow-gray-900/20",
"hover:border-blue-200 dark:hover:border-blue-900", "hover:shadow-lg dark:hover:shadow-gray-900/30",
"min-h-[280px] flex flex-col" "hover:border-blue-300 dark:hover:border-blue-600",
"hover:bg-gray-50/50 dark:hover:bg-gray-800/50",
"hover:-translate-y-1",
"min-h-[200px] flex flex-col"
), ),
}; };
@ -48,45 +47,9 @@ export function ToolkitCard({
onProjectConfigUpdate, onProjectConfigUpdate,
onRemoveToolkitTools onRemoveToolkitTools
}: ToolkitCardProps) { }: ToolkitCardProps) {
const [isProcessing, setIsProcessing] = useState(false); const handleCardClick = useCallback(() => {
const [error, setError] = useState<string | null>(null); onManageTools();
const [showAuthModal, setShowAuthModal] = useState(false); }, [onManageTools]);
const handleToggleConnection = useCallback(async () => {
const newState = !isConnected;
// Clear any previous error when starting a new operation
setError(null);
if (newState) {
// Show authentication modal
setShowAuthModal(true);
} else {
// Disconnect - remove the connected account
setIsProcessing(true);
try {
if (connectedAccountId) {
await deleteConnectedAccount(projectId, toolkit.slug, connectedAccountId);
onProjectConfigUpdate();
onRemoveToolkitTools(toolkit.slug);
} else {
// Fallback: just refresh the project config
onProjectConfigUpdate();
}
} catch (err: any) {
console.error('Disconnect failed:', err);
const errorMessage = err.message || 'Failed to disconnect toolkit';
setError(errorMessage);
} finally {
setIsProcessing(false);
}
}
}, [projectId, toolkit.slug, isConnected, connectedAccountId, onProjectConfigUpdate, onRemoveToolkitTools]);
const handleAuthComplete = useCallback(() => {
// Update project config when authentication completes
onProjectConfigUpdate();
}, [onProjectConfigUpdate]);
// Calculate selected tools count for this toolkit // Calculate selected tools count for this toolkit
const selectedToolsCount = projectConfig?.composioSelectedTools?.filter(tool => const selectedToolsCount = projectConfig?.composioSelectedTools?.filter(tool =>
@ -94,127 +57,70 @@ export function ToolkitCard({
).length || 0; ).length || 0;
return ( return (
<div className={toolkitCardStyles.base}> <div className={toolkitCardStyles.base} onClick={handleCardClick}>
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex items-start justify-between mb-4"> {/* Header */}
<div className="flex items-center gap-3"> <div className="flex items-start gap-3 mb-4">
{toolkit.meta.logo && ( {toolkit.meta.logo && (
<PictureImg <PictureImg
src={toolkit.meta.logo} src={toolkit.meta.logo}
alt={`${toolkit.name} logo`} alt={`${toolkit.name} logo`}
className="w-8 h-8 rounded-md object-cover" className="w-8 h-8 rounded-md object-cover flex-shrink-0"
/> />
)} )}
<div> <div className="flex-1 min-w-0">
<h3 className="font-semibold text-lg text-gray-900 dark:text-gray-100"> <h3 className="font-semibold text-lg text-gray-900 dark:text-gray-100 truncate">
{toolkit.name} {toolkit.name}
</h3> </h3>
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1 flex-wrap">
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium <Chip
bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300"> color="secondary"
{toolkit.meta.tools_count} tools variant="faded"
</span> size="sm"
{selectedToolsCount > 0 && ( >
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium {selectedToolsCount > 0
bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300"> ? `${toolkit.meta.tools_count} tools, ${selectedToolsCount} selected`
{selectedToolsCount} selected : `${toolkit.meta.tools_count} tools`
</span> }
)} </Chip>
{toolkit.no_auth && (
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium
bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300">
No Auth
</span>
)}
</div>
</div> </div>
</div> </div>
<div className="flex items-center gap-2">
{toolkit.no_auth ? (
<div className="flex items-center gap-2">
<Switch
checked={true}
onCheckedChange={() => {}} // No-op for no-auth toolkits
disabled={true}
className={clsx(
"data-[state=checked]:bg-emerald-500 dark:data-[state=checked]:bg-emerald-600",
"data-[state=unchecked]:bg-emerald-500 dark:data-[state=unchecked]:bg-emerald-600",
"opacity-50 cursor-not-allowed",
"scale-75"
)}
/>
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">
Always Available
</span>
</div>
) : (
<Switch
checked={isConnected}
onCheckedChange={handleToggleConnection}
disabled={isProcessing}
className={clsx(
"data-[state=checked]:bg-blue-500 dark:data-[state=checked]:bg-blue-600",
"data-[state=unchecked]:bg-gray-200 dark:data-[state=unchecked]:bg-gray-700",
isProcessing && "opacity-50 cursor-not-allowed",
"scale-75"
)}
/>
)}
</div>
</div> </div>
{/* Description */}
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4 line-clamp-3"> <p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-3">
{toolkit.meta.description} {toolkit.meta.description}
</p> </p>
</div> </div>
<div className="mt-auto"> {/* Footer */}
<div className="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-xs text-gray-400 dark:text-gray-500">
ID: {toolkit.slug}
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isProcessing && ( {isConnected && !toolkit.no_auth && (
<div className="flex items-center gap-1 text-xs py-1 px-2 rounded-full text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20"> <Chip
<Spinner size="sm" /> color='success'
<span>Processing...</span> variant='flat'
</div> size="sm"
startContent={<LinkIcon className="w-3 h-3 mr-1" />}
>
Connected
</Chip>
)} )}
{(isConnected || toolkit.no_auth) && !isProcessing && ( {toolkit.no_auth && (
<div className="text-xs py-1 px-2 rounded-full text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-900/20"> <Chip
{toolkit.no_auth ? 'Available' : 'Connected'} color='success'
</div> variant='flat'
size="sm"
>
Ready
</Chip>
)} )}
{error && (
<div className="text-xs py-1 px-2 rounded-full text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20">
Error: {error}
</div>
)}
<Button
size="sm"
variant="secondary"
onClick={onManageTools}
className="text-xs"
>
<div className="inline-flex items-center">
<Wrench className="h-3.5 w-3.5" />
<span className="ml-1.5">Tools</span>
</div>
</Button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<ToolkitAuthModal
key={toolkit.slug}
isOpen={showAuthModal}
onClose={() => setShowAuthModal(false)}
toolkitSlug={toolkit.slug}
projectId={projectId}
onComplete={handleAuthComplete}
/>
</div> </div>
); );
} }