From 50b69aadbf4a526fed3088720dda106655b25e5b Mon Sep 17 00:00:00 2001 From: Roberto Musso Date: Fri, 27 Feb 2026 23:56:08 +0100 Subject: [PATCH] feat(tasks): register section for floating AI chat in TasksPage --- src/renderer/components/ai/FloatingChat.tsx | 280 ++++++++------------ src/renderer/routes/tasks.tsx | 13 +- 2 files changed, 115 insertions(+), 178 deletions(-) diff --git a/src/renderer/components/ai/FloatingChat.tsx b/src/renderer/components/ai/FloatingChat.tsx index bc30cb5..bd175e5 100644 --- a/src/renderer/components/ai/FloatingChat.tsx +++ b/src/renderer/components/ai/FloatingChat.tsx @@ -1,8 +1,8 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { createPortal } from 'react-dom'; import { AnimatePresence, motion } from 'framer-motion'; import { useRouterState } from '@tanstack/react-router'; -import { X, ArrowUp, Sparkles, GripHorizontal } from 'lucide-react'; +import { X, ArrowUp } from 'lucide-react'; import { useFloatingChat, CHAT_WIDTH, @@ -11,17 +11,9 @@ import { } from '@/context/FloatingChatContext'; import { useAIChat, type ChatContext } from '@/hooks/useAIChat'; import { ChatMarkdown } from '@/components/ai/AIChatPanel'; -import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Skeleton } from '@/components/ui/skeleton'; -interface DragState { - startX: number; - startY: number; - originX: number; - originY: number; -} - function FloatingChatInner() { const { state, sections, close } = useFloatingChat(); const routerState = useRouterState(); @@ -50,69 +42,7 @@ function FloatingChatInner() { clearMessages, } = useAIChat(chatContext); - // ---- Position & drag state ---- - const containerRef = useRef(null); - const headerRef = useRef(null); - const dragRef = useRef(null); - const posRef = useRef({ x: state.position.x, y: state.position.y }); - const [posState, setPosState] = useState({ - x: state.position.x, - y: state.position.y, - }); - - // Sync from context when position changes externally (new section opened) - useEffect(() => { - posRef.current = { x: state.position.x, y: state.position.y }; - setPosState({ x: state.position.x, y: state.position.y }); - }, [state.position.x, state.position.y]); - - // ---- Drag handlers ---- - - const onPointerDown = useCallback((e: React.PointerEvent) => { - e.preventDefault(); - headerRef.current?.setPointerCapture(e.pointerId); - dragRef.current = { - startX: e.clientX, - startY: e.clientY, - originX: posRef.current.x, - originY: posRef.current.y, - }; - }, []); - - const onPointerMove = useCallback((e: React.PointerEvent) => { - const d = dragRef.current; - if (!d) return; - - const dx = e.clientX - d.startX; - const dy = e.clientY - d.startY; - - let newX = d.originX + dx; - let newY = d.originY + dy; - - newX = Math.max( - PADDING, - Math.min(newX, window.innerWidth - CHAT_WIDTH - PADDING), - ); - newY = Math.max( - PADDING, - Math.min(newY, window.innerHeight - CHAT_HEIGHT - PADDING), - ); - - posRef.current = { x: newX, y: newY }; - - const el = containerRef.current; - if (el) { - el.style.left = `${newX}px`; - el.style.top = `${newY}px`; - } - }, []); - - const onPointerUp = useCallback(() => { - if (!dragRef.current) return; - dragRef.current = null; - setPosState({ ...posRef.current }); - }, []); // ---- Close on Escape ---- @@ -153,21 +83,19 @@ function FloatingChatInner() { useEffect(() => { if (!state.isOpen) return; const handler = () => { - const pos = posRef.current; - const clampedX = Math.max( - PADDING, - Math.min(pos.x, window.innerWidth - CHAT_WIDTH - PADDING), - ); - const clampedY = Math.max( - PADDING, - Math.min(pos.y, window.innerHeight - CHAT_HEIGHT - PADDING), - ); - posRef.current = { x: clampedX, y: clampedY }; - setPosState({ x: clampedX, y: clampedY }); + // Re-anchor if the container would go offscreen + const el = containerRef.current; + if (el) { + const rect = el.getBoundingClientRect(); + if (rect.right > window.innerWidth || rect.bottom > window.innerHeight) { + el.style.left = `${Math.max(PADDING, Math.min(state.position.x, window.innerWidth - CHAT_WIDTH - PADDING))}px`; + el.style.top = `${Math.max(PADDING, Math.min(state.position.y, window.innerHeight - CHAT_HEIGHT - PADDING))}px`; + } + } }; window.addEventListener('resize', handler); return () => window.removeEventListener('resize', handler); - }, [state.isOpen]); + }, [state.isOpen, state.position.x, state.position.y]); // ---- Auto-scroll messages ---- @@ -200,6 +128,8 @@ function FloatingChatInner() { } }; + const hasMessages = messages.length > 0 || isStreaming; + return ( {state.isOpen && ( @@ -208,123 +138,121 @@ function FloatingChatInner() { key="floating-chat" layout layoutId={state.morphTargetId ?? undefined} - initial={{ opacity: 0, scale: 0.92, y: 8 }} + initial={{ opacity: 0, scale: 0.95, y: 12 }} animate={{ opacity: 1, scale: 1, y: 0 }} - exit={{ opacity: 0, scale: 0.92, y: 8 }} + exit={{ opacity: 0, scale: 0.95, y: 12 }} transition={{ type: 'spring', stiffness: 400, damping: 30 }} style={{ position: 'fixed', - left: posState.x, - top: posState.y, + left: state.position.x, + top: state.position.y, width: state.position.width, - height: CHAT_HEIGHT, zIndex: 9999, }} - className="rounded-xl border border-border bg-background/95 backdrop-blur-xl shadow-2xl flex flex-col overflow-hidden" + className="flex flex-col gap-2" > - {/* ---- Header ---- */} -
- - - {activeSection?.label ?? 'Chat'} - + {/* ---- Messages panel (appears when chat has content) ---- */} + + {hasMessages && ( + + +
+ {messages.map((msg) => { + if (msg.role === 'user') { + return ( +
+
+

+ {msg.content} +

+
+
+ ); + } + + if (msg.error) { + return ( +
+
+

+ {msg.content} +

+
+
+ ); + } + + return ( +
+
+
+ +
+
+
+ ); + })} + + {/* Streaming */} + {isStreaming && ( +
+
+ {streamingContent ? ( +
+ +
+ ) : ( +
+ + +
+ )} +
+
+ )} +
+
+
+ )} +
+ + {/* ---- Floating input bar ---- */} +
+ {/* Close button */} -
- {/* ---- Messages ---- */} - -
- {messages.map((msg) => { - if (msg.role === 'user') { - return ( -
-
-

- {msg.content} -

-
-
- ); - } - - if (msg.error) { - return ( -
-

- {msg.content} -

-
- ); - } - - return ( -
-
- - Adiuva -
-
- -
-
- ); - })} - - {/* Streaming indicator */} - {isStreaming && ( -
-
- - Adiuva -
- {streamingContent ? ( -
- -
- ) : ( -
- - -
- )} -
- )} -
-
- - {/* ---- Input bar ---- */} -
-
+