269 lines
11 KiB
TypeScript
Raw Normal View History

2025-02-05 00:07:38 +05:30
'use client';
import React, { useState, useEffect, useCallback } from 'react';
import { LogOut, FileCode, FilePlus2, SquareTerminal, TvMinimalPlay } from 'lucide-react';
import Cookies from 'js-cookie';
import { usePrivy, useWallets } from '@privy-io/react-auth';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/common/alert-dialog";
import useDnDStore from '@/stores/useDnDStore';
import Avatar, { genConfig } from 'react-nice-avatar';
import { generateDisplayCode, generateExecutionCode } from '@/utils/exportUtils';
import { NICKNAMES } from '@/utils/nicknames';
import SlidingModal from './SlidingModal';
interface UtilBarProps {
onTutorialClick: () => void;
}
const UtilBar = ({ onTutorialClick }: UtilBarProps) => {
// State management for UI elements
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const [showProfileMenu, setShowProfileMenu] = useState(false);
const [showNewFileDialog, setShowNewFileDialog] = useState(false);
const [userName, setUserName] = useState('User');
const [avatarConfig, setAvatarConfig] = useState(() => genConfig());
// State management for code execution
const [showEditor, setShowEditor] = useState(false);
const [output, setOutput] = useState<string>('');
const [error, setError] = useState<string | null>(null);
const [isExecuting, setIsExecuting] = useState(false);
// Global state and authentication hooks
const { logout: privyLogout } = usePrivy();
const { wallets } = useWallets();
const { nodes, edges, clearNodes } = useDnDStore();
// Initialize random username and avatar on component mount
useEffect(() => {
const randomIndex = Math.floor(Math.random() * NICKNAMES.length);
const selectedName = NICKNAMES[randomIndex];
setUserName(selectedName);
setAvatarConfig(genConfig(selectedName));
}, []);
// File operations handlers
const handleNewFile = useCallback(() => {
clearNodes();
setShowNewFileDialog(false);
}, [clearNodes]);
const handleDownload = useCallback(() => {
// Generate masked version of code for download
const pythonCode = generateDisplayCode(nodes, edges);
const blob = new Blob([pythonCode], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'dotflow.py';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, [nodes, edges]);
// Python code execution handler
const handleRunPython = useCallback(async () => {
setIsExecuting(true);
setError(null);
setShowEditor(true);
setOutput('Executing...'); // Set initial executing state
try {
// Generate code with real credentials for execution
const executionCode = generateExecutionCode(nodes, edges);
const response = await fetch('/api/python', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code: executionCode }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: { output: string } = await response.json();
// Sanitize the output to remove sensitive information
const sanitizedOutput = data.output
.replace(/sk-proj-[a-zA-Z0-9-]+/g, '[API_KEY]')
.replace(/asst_[a-zA-Z0-9]+/g, '[ASSISTANT_ID]');
setOutput(sanitizedOutput);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';
setError(errorMessage);
setOutput(errorMessage);
console.error('Error running Python code:', err);
} finally {
setIsExecuting(false);
}
}, [nodes, edges]);
// Authentication handlers
const handleLogout = async () => {
try {
// Disconnect all connected wallets
for (const wallet of wallets) {
try {
await wallet.disconnect();
} catch (e) {
console.error('Error disconnecting wallet:', e);
}
}
// Clear all authentication related cookies and storage
Cookies.remove('privy-authenticated', { path: '/' });
localStorage.removeItem('useremail');
localStorage.removeItem('privy:embedded-wallet:iframe-ready');
localStorage.removeItem('privy:embedded-wallet:ready');
await privyLogout();
window.location.href = '/';
} catch (error) {
console.error('Logout error:', error);
window.location.href = '/';
}
};
// Tooltip component for action buttons
const IconWithTooltip: React.FC<{ children: React.ReactNode; tooltip: string }> = ({ children, tooltip }) => (
<div className="group relative">
{children}
<div className="absolute -bottom-8 left-1/2 transform -translate-x-1/2 hidden group-hover:block bg-[#252525] text-white text-xs px-2 py-1 rounded whitespace-nowrap border border-white/10">
{tooltip}
</div>
</div>
);
// Action buttons configuration
const actions = [
{ onClick: () => setShowNewFileDialog(true), tooltip: "New File", icon: <FilePlus2 size={19} /> },
{ onClick: handleDownload, tooltip: "Export as Python", icon: <FileCode size={19} /> },
{ onClick: handleRunPython, tooltip: "Run", icon: <SquareTerminal size={19} /> },
{ onClick: onTutorialClick, tooltip: "Tutorial", icon: <TvMinimalPlay size={19} /> },
];
return (
<div className="absolute top-3 right-3 left-0 z-50 p-2 flex justify-end items-center gap-2">
{/* Action buttons */}
<div className="flex items-center gap-3 bg-[#252525]/80 backdrop-blur-md px-4 py-2 rounded-full border border-white/5">
{actions.map((action, index) => (
<React.Fragment key={index}>
<button
onClick={action.onClick}
className="flex items-center gap-1 text-gray-300 hover:text-white text-sm transition-transform duration-200 hover:scale-105"
disabled={action.tooltip === "Run" && isExecuting}
>
<IconWithTooltip tooltip={action.tooltip}>{action.icon}</IconWithTooltip>
</button>
{index < actions.length - 1 && <div className="w-px h-4 bg-gray-600/50" />}
</React.Fragment>
))}
</div>
{/* Profile menu */}
<div className="relative">
<button
className="p-1.5 bg-[#252525]/80 backdrop-blur-md rounded-full transition-all duration-200 hover:scale-105 border border-white/5 flex items-center gap-1 pr-1.5 pl-3"
onMouseEnter={() => setShowProfileMenu(true)}
onMouseLeave={() => setShowProfileMenu(false)}
>
<div className='text-sm text-gray-300 mr-2 font-offbit'>
{userName}
</div>
<Avatar style={{ width: '24px', height: '24px' }} {...avatarConfig} />
</button>
{showProfileMenu && (
<div
className="absolute right-0 -mt-1 w-35 rounded-xl bg-[#252525]/90 backdrop-blur-md shadow-2xl py-2 border border-white/10 transform transition-all duration-200 ease-out hover:bg-gradient-to-r from-red-500/10 via-red-500/5 to-transparent"
onMouseEnter={() => setShowProfileMenu(true)}
onMouseLeave={() => setShowProfileMenu(false)}
>
<button
onClick={() => setShowLogoutDialog(true)}
className="flex items-center w-full px-4 py-1 text-sm text-gray-300 hover:text-white transition-all duration-200"
>
<LogOut className="h-4 w-4 mr-3" />
<span className="font-medium">Logout</span>
</button>
</div>
)}
</div>
{/* Logout confirmation dialog */}
<AlertDialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<AlertDialogContent className="bg-[#252525]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl">
<AlertDialogHeader>
<AlertDialogTitle className="text-xl font-semibold bg-gradient-to-r from-white via-white to-gray-300 bg-clip-text text-transparent">
Ready to leave?
</AlertDialogTitle>
<AlertDialogDescription className="text-gray-400 mt-2">
Logging out will disconnect your wallets and end your current session. You can always log back in later.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="mt-6">
<AlertDialogCancel className="bg-[#333333]/50 text-white hover:bg-[#404040]/50 border border-white/5 px-5 rounded-xl transition-all duration-200 hover:scale-105">
Stay
</AlertDialogCancel>
<AlertDialogAction
onClick={handleLogout}
className="bg-gradient-to-r from-red-500 to-red-600 text-white hover:from-red-600 hover:to-red-700 border-none px-5 rounded-xl transition-all duration-200 hover:scale-105 shadow-lg hover:shadow-red-500/25"
>
Logout
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* New file confirmation dialog */}
<AlertDialog open={showNewFileDialog} onOpenChange={setShowNewFileDialog}>
<AlertDialogContent className="bg-[#252525]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl">
<AlertDialogHeader>
<AlertDialogTitle className="text-xl font-semibold bg-gradient-to-r from-white via-white to-gray-300 bg-clip-text text-transparent">
Create New File?
</AlertDialogTitle>
<AlertDialogDescription className="text-gray-400 mt-2">
This will clear your current work. Are you sure you want to continue?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="mt-6">
<AlertDialogCancel className="bg-[#333333]/50 text-white hover:bg-[#404040]/50 border border-white/5 px-5 rounded-xl transition-all duration-200 hover:scale-105">
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={handleNewFile}
className="bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none px-5 rounded-xl transition-all duration-200 hover:scale-105 shadow-lg hover:shadow-blue-500/25"
>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Code editor modal */}
<SlidingModal
isOpen={showEditor}
onClose={() => setShowEditor(false)}
pythonCode={generateDisplayCode(nodes, edges)}
output={error || output}
isExecuting={isExecuting}
/>
</div>
);
};
export default UtilBar;