269 lines
11 KiB
TypeScript
269 lines
11 KiB
TypeScript
![]() |
'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;
|