mirror of https://github.com/kortix-ai/suna.git
Merge pull request #518 from kubet/fix/pointer-native-event
Fix/pointer native event
This commit is contained in:
commit
606bff01f0
|
@ -61,38 +61,38 @@ export function NavAgents() {
|
||||||
const { performDelete } = useDeleteOperation();
|
const { performDelete } = useDeleteOperation();
|
||||||
const isPerformingActionRef = useRef(false);
|
const isPerformingActionRef = useRef(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [selectedThreads, setSelectedThreads] = useState<Set<string>>(new Set());
|
const [selectedThreads, setSelectedThreads] = useState<Set<string>>(new Set());
|
||||||
const [deleteProgress, setDeleteProgress] = useState(0);
|
const [deleteProgress, setDeleteProgress] = useState(0);
|
||||||
const [totalToDelete, setTotalToDelete] = useState(0);
|
const [totalToDelete, setTotalToDelete] = useState(0);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: projects = [],
|
data: projects = [],
|
||||||
isLoading: isProjectsLoading,
|
isLoading: isProjectsLoading,
|
||||||
error: projectsError
|
error: projectsError
|
||||||
} = useProjects();
|
} = useProjects();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: threads = [],
|
data: threads = [],
|
||||||
isLoading: isThreadsLoading,
|
isLoading: isThreadsLoading,
|
||||||
error: threadsError
|
error: threadsError
|
||||||
} = useThreads();
|
} = useThreads();
|
||||||
|
|
||||||
const { mutate: deleteThreadMutation, isPending: isDeletingSingle } = useDeleteThread();
|
const { mutate: deleteThreadMutation, isPending: isDeletingSingle } = useDeleteThread();
|
||||||
const {
|
const {
|
||||||
mutate: deleteMultipleThreadsMutation,
|
mutate: deleteMultipleThreadsMutation,
|
||||||
isPending: isDeletingMultiple
|
isPending: isDeletingMultiple
|
||||||
} = useDeleteMultipleThreads();
|
} = useDeleteMultipleThreads();
|
||||||
|
|
||||||
const combinedThreads: ThreadWithProject[] =
|
const combinedThreads: ThreadWithProject[] =
|
||||||
!isProjectsLoading && !isThreadsLoading ?
|
!isProjectsLoading && !isThreadsLoading ?
|
||||||
processThreadsWithProjects(threads, projects) : [];
|
processThreadsWithProjects(threads, projects) : [];
|
||||||
|
|
||||||
const handleDeletionProgress = (completed: number, total: number) => {
|
const handleDeletionProgress = (completed: number, total: number) => {
|
||||||
const percentage = (completed / total) * 100;
|
const percentage = (completed / total) * 100;
|
||||||
setDeleteProgress(percentage);
|
setDeleteProgress(percentage);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleProjectUpdate = (event: Event) => {
|
const handleProjectUpdate = (event: Event) => {
|
||||||
const customEvent = event as CustomEvent;
|
const customEvent = event as CustomEvent;
|
||||||
|
@ -145,7 +145,7 @@ export function NavAgents() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setLoadingThreadId(threadId)
|
setLoadingThreadId(threadId)
|
||||||
router.push(url)
|
router.push(url)
|
||||||
|
@ -157,7 +157,7 @@ export function NavAgents() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedThreads(prev => {
|
setSelectedThreads(prev => {
|
||||||
const newSelection = new Set(prev);
|
const newSelection = new Set(prev);
|
||||||
if (newSelection.has(threadId)) {
|
if (newSelection.has(threadId)) {
|
||||||
|
@ -189,18 +189,18 @@ export function NavAgents() {
|
||||||
// Function to handle multi-delete
|
// Function to handle multi-delete
|
||||||
const handleMultiDelete = () => {
|
const handleMultiDelete = () => {
|
||||||
if (selectedThreads.size === 0) return;
|
if (selectedThreads.size === 0) return;
|
||||||
|
|
||||||
// Get thread names for confirmation dialog
|
// Get thread names for confirmation dialog
|
||||||
const threadsToDelete = combinedThreads.filter(t => selectedThreads.has(t.threadId));
|
const threadsToDelete = combinedThreads.filter(t => selectedThreads.has(t.threadId));
|
||||||
const threadNames = threadsToDelete.map(t => t.projectName).join(", ");
|
const threadNames = threadsToDelete.map(t => t.projectName).join(", ");
|
||||||
|
|
||||||
setThreadToDelete({
|
setThreadToDelete({
|
||||||
id: "multiple",
|
id: "multiple",
|
||||||
name: selectedThreads.size > 3
|
name: selectedThreads.size > 3
|
||||||
? `${selectedThreads.size} conversations`
|
? `${selectedThreads.size} conversations`
|
||||||
: threadNames
|
: threadNames
|
||||||
});
|
});
|
||||||
|
|
||||||
setTotalToDelete(selectedThreads.size);
|
setTotalToDelete(selectedThreads.size);
|
||||||
setDeleteProgress(0);
|
setDeleteProgress(0);
|
||||||
setIsDeleteDialogOpen(true);
|
setIsDeleteDialogOpen(true);
|
||||||
|
@ -261,10 +261,10 @@ export function NavAgents() {
|
||||||
// Multi-thread deletion
|
// Multi-thread deletion
|
||||||
const threadIdsToDelete = Array.from(selectedThreads);
|
const threadIdsToDelete = Array.from(selectedThreads);
|
||||||
const isActiveThreadIncluded = threadIdsToDelete.some(id => pathname?.includes(id));
|
const isActiveThreadIncluded = threadIdsToDelete.some(id => pathname?.includes(id));
|
||||||
|
|
||||||
// Show initial toast
|
// Show initial toast
|
||||||
toast.info(`Deleting ${threadIdsToDelete.length} conversations...`);
|
toast.info(`Deleting ${threadIdsToDelete.length} conversations...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// If the active thread is included, handle navigation first
|
// If the active thread is included, handle navigation first
|
||||||
if (isActiveThreadIncluded) {
|
if (isActiveThreadIncluded) {
|
||||||
|
@ -272,14 +272,14 @@ export function NavAgents() {
|
||||||
isNavigatingRef.current = true;
|
isNavigatingRef.current = true;
|
||||||
document.body.style.pointerEvents = 'none';
|
document.body.style.pointerEvents = 'none';
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
|
|
||||||
// Wait a moment for navigation to start
|
// Wait a moment for navigation to start
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the mutation for bulk deletion
|
// Use the mutation for bulk deletion
|
||||||
deleteMultipleThreadsMutation(
|
deleteMultipleThreadsMutation(
|
||||||
{
|
{
|
||||||
threadIds: threadIdsToDelete,
|
threadIds: threadIdsToDelete,
|
||||||
onProgress: handleDeletionProgress
|
onProgress: handleDeletionProgress
|
||||||
},
|
},
|
||||||
|
@ -287,15 +287,15 @@ export function NavAgents() {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
// Invalidate queries to refresh the list
|
// Invalidate queries to refresh the list
|
||||||
queryClient.invalidateQueries({ queryKey: threadKeys.lists() });
|
queryClient.invalidateQueries({ queryKey: threadKeys.lists() });
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
toast.success(`Successfully deleted ${data.successful.length} conversations`);
|
toast.success(`Successfully deleted ${data.successful.length} conversations`);
|
||||||
|
|
||||||
// If some deletions failed, show warning
|
// If some deletions failed, show warning
|
||||||
if (data.failed.length > 0) {
|
if (data.failed.length > 0) {
|
||||||
toast.warning(`Failed to delete ${data.failed.length} conversations`);
|
toast.warning(`Failed to delete ${data.failed.length} conversations`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset states
|
// Reset states
|
||||||
setSelectedThreads(new Set());
|
setSelectedThreads(new Set());
|
||||||
setDeleteProgress(0);
|
setDeleteProgress(0);
|
||||||
|
@ -316,7 +316,7 @@ export function NavAgents() {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error initiating bulk deletion:', err);
|
console.error('Error initiating bulk deletion:', err);
|
||||||
toast.error('Error initiating deletion process');
|
toast.error('Error initiating deletion process');
|
||||||
|
|
||||||
// Reset states
|
// Reset states
|
||||||
setSelectedThreads(new Set());
|
setSelectedThreads(new Set());
|
||||||
setThreadToDelete(null);
|
setThreadToDelete(null);
|
||||||
|
@ -330,7 +330,7 @@ export function NavAgents() {
|
||||||
// Loading state or error handling
|
// Loading state or error handling
|
||||||
const isLoading = isProjectsLoading || isThreadsLoading;
|
const isLoading = isProjectsLoading || isThreadsLoading;
|
||||||
const hasError = projectsError || threadsError;
|
const hasError = projectsError || threadsError;
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
console.error('Error loading data:', { projectsError, threadsError });
|
console.error('Error loading data:', { projectsError, threadsError });
|
||||||
}
|
}
|
||||||
|
@ -343,16 +343,16 @@ export function NavAgents() {
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
{selectedThreads.size > 0 ? (
|
{selectedThreads.size > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={deselectAllThreads}
|
onClick={deselectAllThreads}
|
||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={selectAllThreads}
|
onClick={selectAllThreads}
|
||||||
disabled={selectedThreads.size === combinedThreads.length}
|
disabled={selectedThreads.size === combinedThreads.length}
|
||||||
|
@ -360,8 +360,8 @@ export function NavAgents() {
|
||||||
>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleMultiDelete}
|
onClick={handleMultiDelete}
|
||||||
className="h-7 w-7 text-destructive"
|
className="h-7 w-7 text-destructive"
|
||||||
|
@ -436,8 +436,8 @@ export function NavAgents() {
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
asChild
|
asChild
|
||||||
className={
|
className={
|
||||||
isActive ? 'bg-accent text-accent-foreground' :
|
isActive ? 'bg-accent text-accent-foreground' :
|
||||||
isSelected ? 'bg-primary/10' : ''
|
isSelected ? 'bg-primary/10' : ''
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
|
@ -462,13 +462,12 @@ export function NavAgents() {
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
asChild
|
asChild
|
||||||
className={`relative ${
|
className={`relative ${isActive
|
||||||
isActive
|
? 'bg-accent text-accent-foreground font-medium'
|
||||||
? 'bg-accent text-accent-foreground font-medium'
|
: isSelected
|
||||||
: isSelected
|
? 'bg-primary/10'
|
||||||
? 'bg-primary/10'
|
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={thread.url}
|
href={thread.url}
|
||||||
|
@ -484,27 +483,24 @@ export function NavAgents() {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* MessagesSquare icon - hidden on hover if not selected */}
|
{/* MessagesSquare icon - hidden on hover if not selected */}
|
||||||
<MessagesSquare
|
<MessagesSquare
|
||||||
className={`h-4 w-4 transition-opacity duration-150 ${
|
className={`h-4 w-4 transition-opacity duration-150 ${isSelected ? 'opacity-0' : 'opacity-100 group-hover/icon:opacity-0'
|
||||||
isSelected ? 'opacity-0' : 'opacity-100 group-hover/icon:opacity-0'
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Checkbox - appears on hover or when selected */}
|
{/* Checkbox - appears on hover or when selected */}
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-150 ${
|
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-150 ${isSelected
|
||||||
isSelected
|
? 'opacity-100'
|
||||||
? 'opacity-100'
|
: 'opacity-0 group-hover/icon:opacity-100'
|
||||||
: 'opacity-0 group-hover/icon:opacity-100'
|
}`}
|
||||||
}`}
|
|
||||||
onClick={(e) => toggleThreadSelection(thread.threadId, e)}
|
onClick={(e) => toggleThreadSelection(thread.threadId, e)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`h-4 w-4 border rounded cursor-pointer hover:bg-muted/50 transition-colors flex items-center justify-center ${
|
className={`h-4 w-4 border rounded cursor-pointer hover:bg-muted/50 transition-colors flex items-center justify-center ${isSelected
|
||||||
isSelected
|
? 'bg-primary border-primary'
|
||||||
? 'bg-primary border-primary'
|
: 'border-muted-foreground/30 bg-background'
|
||||||
: 'border-muted-foreground/30 bg-background'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{isSelected && <Check className="h-3 w-3 text-primary-foreground" />}
|
{isSelected && <Check className="h-3 w-3 text-primary-foreground" />}
|
||||||
</div>
|
</div>
|
||||||
|
@ -520,7 +516,14 @@ export function NavAgents() {
|
||||||
{state !== 'collapsed' && !isSelected && (
|
{state !== 'collapsed' && !isSelected && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuAction showOnHover className="group-hover:opacity-100">
|
<SidebarMenuAction
|
||||||
|
showOnHover
|
||||||
|
className="group-hover:opacity-100"
|
||||||
|
onClick={() => {
|
||||||
|
// Ensure pointer events are enabled when dropdown opens
|
||||||
|
document.body.style.pointerEvents = 'auto';
|
||||||
|
}}
|
||||||
|
>
|
||||||
<MoreHorizontal />
|
<MoreHorizontal />
|
||||||
<span className="sr-only">More</span>
|
<span className="sr-only">More</span>
|
||||||
</SidebarMenuAction>
|
</SidebarMenuAction>
|
||||||
|
@ -575,21 +578,21 @@ export function NavAgents() {
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
)}
|
)}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
|
|
||||||
{(isDeletingSingle || isDeletingMultiple) && totalToDelete > 0 && (
|
{(isDeletingSingle || isDeletingMultiple) && totalToDelete > 0 && (
|
||||||
<div className="mt-2 px-2">
|
<div className="mt-2 px-2">
|
||||||
<div className="text-xs text-muted-foreground mb-1">
|
<div className="text-xs text-muted-foreground mb-1">
|
||||||
Deleting {deleteProgress > 0 ? `(${Math.floor(deleteProgress)}%)` : '...'}
|
Deleting {deleteProgress > 0 ? `(${Math.floor(deleteProgress)}%)` : '...'}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-secondary h-1 rounded-full overflow-hidden">
|
<div className="w-full bg-secondary h-1 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="bg-primary h-1 transition-all duration-300 ease-in-out"
|
className="bg-primary h-1 transition-all duration-300 ease-in-out"
|
||||||
style={{ width: `${deleteProgress}%` }}
|
style={{ width: `${deleteProgress}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ShareModal
|
<ShareModal
|
||||||
isOpen={showShareModal}
|
isOpen={showShareModal}
|
||||||
onClose={() => setShowShareModal(false)}
|
onClose={() => setShowShareModal(false)}
|
||||||
|
|
|
@ -28,7 +28,14 @@ export function ShareModal({ isOpen, onClose, threadId, projectId }: ShareModalP
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isChecking, setIsChecking] = useState(false);
|
const [isChecking, setIsChecking] = useState(false);
|
||||||
const [isCopying, setIsCopying] = useState(false);
|
const [isCopying, setIsCopying] = useState(false);
|
||||||
|
|
||||||
|
// Reset pointer events when modal opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.pointerEvents = 'auto';
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const updateThreadMutation = useUpdateThreadMutation();
|
const updateThreadMutation = useUpdateThreadMutation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -105,7 +112,7 @@ export function ShareModal({ isOpen, onClose, threadId, projectId }: ShareModalP
|
||||||
const updatePublicStatus = async (isPublic: boolean) => {
|
const updatePublicStatus = async (isPublic: boolean) => {
|
||||||
console.log("Updating public status for thread:", threadId, "and project:", projectId, "to", isPublic);
|
console.log("Updating public status for thread:", threadId, "and project:", projectId, "to", isPublic);
|
||||||
if (!threadId) return;
|
if (!threadId) return;
|
||||||
|
|
||||||
await updateProject(projectId, { is_public: isPublic });
|
await updateProject(projectId, { is_public: isPublic });
|
||||||
await updateThreadMutation.mutateAsync({
|
await updateThreadMutation.mutateAsync({
|
||||||
threadId,
|
threadId,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -32,6 +32,13 @@ export function DeleteConfirmationDialog({
|
||||||
threadName,
|
threadName,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
}: DeleteConfirmationDialogProps) {
|
}: DeleteConfirmationDialogProps) {
|
||||||
|
// Reset pointer events when dialog opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.pointerEvents = 'auto';
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog open={isOpen} onOpenChange={onClose}>
|
<AlertDialog open={isOpen} onOpenChange={onClose}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
|
|
|
@ -96,7 +96,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
|
||||||
}, [value, ref]);
|
}, [value, ref]);
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (
|
if (
|
||||||
(value.trim() || uploadedFiles.length > 0) &&
|
(value.trim() || uploadedFiles.length > 0) &&
|
||||||
|
|
Loading…
Reference in New Issue