Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { routeTree } from "./routeTree.gen";
import { useOpenSecret, OpenSecretProvider } from "@opensecret/react";
import { OpenAIProvider } from "./ai/OpenAIContext";
import { LocalStateProvider } from "./state/LocalStateContext";
import { ProjectsProvider } from "./state/ProjectsContext";
import { ErrorFallback } from "./components/ErrorFallback";
import { NotFoundFallback } from "./components/NotFoundFallback";
import { BillingServiceProvider } from "./components/BillingServiceProvider";
Expand Down Expand Up @@ -96,6 +97,7 @@ export default function App() {
}}
>
<LocalStateProvider>
<ProjectsProvider>
<OpenAIProvider>
<QueryClientProvider client={queryClient}>
<TooltipProvider>
Expand All @@ -110,6 +112,7 @@ export default function App() {
</TooltipProvider>
</QueryClientProvider>
</OpenAIProvider>
</ProjectsProvider>
</LocalStateProvider>
</OpenSecretProvider>
</NotificationProvider>
Expand Down
176 changes: 176 additions & 0 deletions frontend/src/components/ChatContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { useState } from "react";
import {
CheckSquare,
ChevronLeft,
ChevronRight,
Folder,
FolderMinus,
FolderPlus,
MoreHorizontal,
Pencil,
Plus,
Trash2
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";

interface ChatContextMenuProps {
chatId: string;
isMobile: boolean;
// All projects for "Move to project" submenu
projects: { id: string; name: string }[];
// If set, shows "Remove from {projectName}" and grays out current project in submenu
currentProjectName?: string;
currentProjectId?: string;
// Optional callbacks — item only rendered when provided
onSelect?: () => void;
onRename?: () => void;
onMoveToProject?: (projectId: string) => void;
onRemoveFromProject?: () => void;
onDelete?: () => void;
}

export function ChatContextMenu({
chatId,
isMobile,
projects,
currentProjectName,
currentProjectId,
onSelect,
onRename,
onMoveToProject,
onRemoveFromProject,
onDelete
}: ChatContextMenuProps) {
const [showProjectSubmenu, setShowProjectSubmenu] = useState(false);

return (
<DropdownMenu onOpenChange={(open) => !open && setShowProjectSubmenu(false)}>
<DropdownMenuTrigger asChild>
<button
className={`z-50 bg-background/80 absolute right-2 top-1/2 transform -translate-y-1/2 text-primary transition-opacity p-2 ${
isMobile ? "opacity-100" : "opacity-0 group-hover:opacity-100"
}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<MoreHorizontal size={16} />
</button>
Comment on lines +55 to +65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Trigger button lacks an accessible label.

The <button> wrapping <MoreHorizontal /> has no aria-label, so screen readers will announce it as an unlabeled button. Add an aria-label for accessibility.

♻️ Suggested fix
         <button
           className={`z-50 bg-background/80 absolute right-2 top-1/2 transform -translate-y-1/2 text-primary transition-opacity p-2 ${
             isMobile ? "opacity-100" : "opacity-0 group-hover:opacity-100"
           }`}
+          aria-label="Chat actions"
           onClick={(e) => {
             e.preventDefault();
             e.stopPropagation();
           }}
         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
className={`z-50 bg-background/80 absolute right-2 top-1/2 transform -translate-y-1/2 text-primary transition-opacity p-2 ${
isMobile ? "opacity-100" : "opacity-0 group-hover:opacity-100"
}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<MoreHorizontal size={16} />
</button>
<button
className={`z-50 bg-background/80 absolute right-2 top-1/2 transform -translate-y-1/2 text-primary transition-opacity p-2 ${
isMobile ? "opacity-100" : "opacity-0 group-hover:opacity-100"
}`}
aria-label="Chat actions"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<MoreHorizontal size={16} />
</button>
🤖 Prompt for AI Agents
In `@frontend/src/components/ChatContextMenu.tsx` around lines 53 - 63, The button
in ChatContextMenu wrapping the MoreHorizontal icon lacks an accessible label,
so update the <button> element inside the ChatContextMenu component to include
an appropriate aria-label (e.g., "Open message menu" or similar) so screen
readers can announce its purpose; locate the button using the MoreHorizontal
icon and isMobile usage and add the aria-label attribute and ensure it remains
descriptive and concise.

</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 overflow-hidden">
<div className="relative overflow-hidden">
{/* Main menu layer — in-flow when active, absolute when hidden */}
<div
className={`transition-transform duration-300 ease-in-out ${
showProjectSubmenu
? "absolute top-0 left-0 w-full -translate-x-[200%]"
: "translate-x-0"
}`}
>
{onSelect && (
<DropdownMenuItem onClick={onSelect}>
<CheckSquare className="mr-2 h-4 w-4" />
<span>Select</span>
</DropdownMenuItem>
)}
{onRename && (
<DropdownMenuItem onClick={onRename}>
<Pencil className="mr-2 h-4 w-4" />
<span>Rename chat</span>
</DropdownMenuItem>
)}
{onMoveToProject && (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setShowProjectSubmenu(true);
}}
onSelect={(e) => e.preventDefault()}
>
<FolderPlus className="mr-2 h-4 w-4" />
<span className="flex-1">Move to project</span>
<ChevronRight className="h-4 w-4 ml-auto" />
</DropdownMenuItem>
)}
{onRemoveFromProject && currentProjectName && (
<DropdownMenuItem onClick={onRemoveFromProject}>
<FolderMinus className="mr-2 h-4 w-4" />
<span>Remove from {currentProjectName}</span>
</DropdownMenuItem>
)}
{onDelete && (
<DropdownMenuItem
onClick={onDelete}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete chat</span>
</DropdownMenuItem>
)}
</div>

{/* Project submenu layer — in-flow when active, absolute when hidden */}
{onMoveToProject && (
<div
className={`transition-transform duration-300 ease-in-out ${
showProjectSubmenu
? "translate-x-0"
: "absolute top-0 left-0 w-full translate-x-[200%]"
}`}
>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setShowProjectSubmenu(false);
}}
onSelect={(e) => e.preventDefault()}
>
<ChevronLeft className="mr-2 h-4 w-4" />
<span>Back</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
window.dispatchEvent(
new CustomEvent("createprojectforchat", {
detail: { chatId }
})
);
}}
>
<Plus className="mr-2 h-4 w-4" />
<span>New project</span>
</DropdownMenuItem>
{projects.length > 0 && <DropdownMenuSeparator />}
<div className="max-h-[40vh] overflow-y-auto">
{projects.map((project) => {
const isCurrent = project.id === currentProjectId;
return (
<DropdownMenuItem
key={project.id}
onClick={() => !isCurrent && onMoveToProject(project.id)}
onSelect={(e) => isCurrent && e.preventDefault()}
disabled={isCurrent}
>
<Folder className="mr-2 h-4 w-4" />
<span className="truncate">{project.name}</span>
</DropdownMenuItem>
);
})}
</div>
</div>
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
);
}
Loading
Loading