feat(floating-ai): step 3 — create double-click hook

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Roberto Musso
2026-02-27 22:49:16 +01:00
parent b4e97e14f3
commit 28a5d65f1a
3 changed files with 56 additions and 3 deletions

View File

@@ -355,7 +355,7 @@ return (
## Step 3: Create Double-Click Hook ## Step 3: Create Double-Click Hook
**Status**: [ ] **Status**: [x] 2026-02-27
**Prerequisites**: Step 2 completed **Prerequisites**: Step 2 completed
**Creates**: `src/renderer/hooks/useDoubleClickAI.ts` **Creates**: `src/renderer/hooks/useDoubleClickAI.ts`
**Modifies**: `src/renderer/components/layout/AppShell.tsx` (add hook call) **Modifies**: `src/renderer/components/layout/AppShell.tsx` (add hook call)

View File

@@ -18,6 +18,7 @@ import {
Palette Palette
} from 'lucide-react'; } from 'lucide-react';
import { trpc } from '@/lib/trpc'; import { trpc } from '@/lib/trpc';
import { useDoubleClickAI } from '@/hooks/useDoubleClickAI';
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -84,6 +85,16 @@ function findScrollableAncestor(el: Element | null): Element | null {
} }
export function AppShell({ children }: AppShellProps) { export function AppShell({ children }: AppShellProps) {
return (
<FloatingChatProvider>
<AppShellInner>{children}</AppShellInner>
</FloatingChatProvider>
);
}
function AppShellInner({ children }: AppShellProps) {
useDoubleClickAI();
const collapsedQuery = trpc.settings.getSidebarCollapsed.useQuery(undefined, { const collapsedQuery = trpc.settings.getSidebarCollapsed.useQuery(undefined, {
staleTime: Infinity, staleTime: Infinity,
}); });
@@ -209,7 +220,6 @@ export function AppShell({ children }: AppShellProps) {
}, [openCurtain, closeCurtain]); }, [openCurtain, closeCurtain]);
return ( return (
<FloatingChatProvider>
<> <>
<SidebarProvider open={open} onOpenChange={handleOpenChange}> <SidebarProvider open={open} onOpenChange={handleOpenChange}>
<AppSidebar <AppSidebar
@@ -301,7 +311,6 @@ export function AppShell({ children }: AppShellProps) {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</> </>
</FloatingChatProvider>
); );
} }

View File

@@ -0,0 +1,44 @@
import { useEffect } from 'react';
import { useFloatingChat } from '@/context/FloatingChatContext';
// Elements where double-click should NOT trigger the AI popup
const INTERACTIVE_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT']);
export function useDoubleClickAI(): void {
const { openAtSection, state } = useFloatingChat();
useEffect(() => {
const handler = (e: MouseEvent) => {
const target = e.target as HTMLElement;
// Skip interactive elements (preserve text selection behavior)
if (INTERACTIVE_TAGS.has(target.tagName)) return;
// Skip contenteditable elements UNLESS they're inside Milkdown
if (target.isContentEditable) {
const inMilkdown =
target.closest('.milkdown-container') ||
target.closest('.crepe-editor');
if (!inMilkdown) return;
// For Milkdown: only trigger if no text was selected by the double-click
const selection = window.getSelection();
if (selection && selection.toString().trim().length > 0) return;
}
// Walk up DOM to find nearest [data-ai-section]
const sectionEl = (target as Element).closest('[data-ai-section]');
if (!sectionEl) return;
const sectionId = sectionEl.getAttribute('data-ai-section');
if (!sectionId) return;
// If popup is already open at THIS section, do nothing
if (state.isOpen && state.activeSectionId === sectionId) return;
openAtSection(sectionId);
};
document.addEventListener('dblclick', handler);
return () => document.removeEventListener('dblclick', handler);
}, [openAtSection, state.isOpen, state.activeSectionId]);
}