diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index e3dc80512378..2b71f913d7f1 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -59,6 +59,7 @@ import { import { type View, ViewOptions } from '../App'; import { MainPanelLayout } from './Layout/MainPanelLayout'; import ChatInput from './ChatInput'; +import ChatInputWrapper from './ChatInputWrapper'; import { ScrollArea, ScrollAreaHandle } from './ui/scroll-area'; import { RecipeWarningModal } from './ui/RecipeWarningModal'; import ParameterInputModal from './ParameterInputModal'; @@ -84,6 +85,7 @@ interface BaseChatProps { enableLocalStorage?: boolean; onMessageStreamFinish?: () => void; onMessageSubmit?: (message: string) => void; // Callback after message is submitted + onTypingStateChange?: (isTyping: boolean) => void; // Callback for typing state changes renderHeader?: () => React.ReactNode; renderBeforeMessages?: () => React.ReactNode; renderAfterMessages?: () => React.ReactNode; @@ -103,6 +105,7 @@ function BaseChatContent({ enableLocalStorage = false, onMessageStreamFinish, onMessageSubmit, + onTypingStateChange, renderHeader, renderBeforeMessages, renderAfterMessages, @@ -353,16 +356,8 @@ function BaseChatContent({ ref={scrollRef} className={`flex-1 rounded-b-2xl min-h-0 relative ${contentClassName}`} style={{ - background: ` - linear-gradient(135deg, - rgba(255, 255, 255, 0.1) 0%, - rgba(255, 255, 255, 0.05) 100% - ) - `, - backdropFilter: 'blur(20px) saturate(180%)', - WebkitBackdropFilter: 'blur(20px) saturate(180%)', + background: 'transparent', border: '1px solid rgba(255, 255, 255, 0.1)', - boxShadow: '0 8px 32px 0 rgba(31, 38, 135, 0.37)', }} autoScroll onDrop={handleDrop} @@ -541,28 +536,30 @@ function BaseChatContent({
- setDroppedFiles([])} // Clear dropped files after processing - messages={messages} - setMessages={setMessages} - disableAnimation={disableAnimation} - sessionCosts={sessionCosts} - setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} - recipeConfig={recipeConfig} - recipeAccepted={recipeAccepted} - initialPrompt={initialPrompt} - {...customChatInputProps} - /> + + setDroppedFiles([])} // Clear dropped files after processing + messages={messages} + setMessages={setMessages} + disableAnimation={disableAnimation} + sessionCosts={sessionCosts} + setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} + recipeConfig={recipeConfig} + recipeAccepted={recipeAccepted} + initialPrompt={initialPrompt} + {...customChatInputProps} + /> +
diff --git a/ui/desktop/src/components/BlurOverlay.tsx b/ui/desktop/src/components/BlurOverlay.tsx new file mode 100644 index 000000000000..f67d0284d928 --- /dev/null +++ b/ui/desktop/src/components/BlurOverlay.tsx @@ -0,0 +1,56 @@ +import React, { useState, useEffect } from 'react'; + +interface BlurOverlayProps { + isActive: boolean; + isDarkMode?: boolean; + intensity?: number; +} + +const BlurOverlay: React.FC = ({ + isActive, + isDarkMode = true, + intensity = 20 // Default blur intensity +}) => { + const [isVisible, setIsVisible] = useState(false); + const [opacity, setOpacity] = useState(0); + + // Use two-step animation for smoother transition + useEffect(() => { + let timeout: NodeJS.Timeout; + + if (isActive) { + setIsVisible(true); + // Small delay before starting the opacity transition + timeout = setTimeout(() => { + setOpacity(1); + }, 10); + } else { + setOpacity(0); + // Wait for transition to complete before hiding + timeout = setTimeout(() => { + setIsVisible(false); + }, 300); // Match the transition duration + } + + return () => { + if (timeout) clearTimeout(timeout); + }; + }, [isActive]); + + if (!isVisible) return null; + + return ( +
+ ); +}; + +export default BlurOverlay; diff --git a/ui/desktop/src/components/ChatInputWrapper.tsx b/ui/desktop/src/components/ChatInputWrapper.tsx new file mode 100644 index 000000000000..6b7cf9223033 --- /dev/null +++ b/ui/desktop/src/components/ChatInputWrapper.tsx @@ -0,0 +1,54 @@ +import React, { useState, useEffect, useRef } from 'react'; + +interface ChatInputWrapperProps { + children: React.ReactNode; + onTypingStateChange?: (isTyping: boolean) => void; +} + +const ChatInputWrapper: React.FC = ({ children, onTypingStateChange }) => { + const [isTyping, setIsTyping] = useState(false); + const timeoutRef = useRef(null); + + // Listen for keyboard input events in the chat input + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Only trigger for textarea elements + if ((e.target as HTMLElement).tagName.toLowerCase() === 'textarea') { + // Set typing state to true + setIsTyping(true); + if (onTypingStateChange) onTypingStateChange(true); + + // Clear any existing timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + // Set a timeout to turn off the typing state after 1.5 seconds of inactivity + timeoutRef.current = setTimeout(() => { + setIsTyping(false); + if (onTypingStateChange) onTypingStateChange(false); + }, 1500); + } + }; + + // Add event listeners + document.addEventListener('keydown', handleKeyDown); + + // Cleanup + return () => { + document.removeEventListener('keydown', handleKeyDown); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, [onTypingStateChange]); + + return ( +
+ {/* Render the chat input */} + {children} +
+ ); +}; + +export default ChatInputWrapper; diff --git a/ui/desktop/src/components/CustomSideNav.tsx b/ui/desktop/src/components/CustomSideNav.tsx new file mode 100644 index 000000000000..77360ddc73e7 --- /dev/null +++ b/ui/desktop/src/components/CustomSideNav.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { cn } from '../utils'; +import { + House, + ChatCenteredText, + Clock, + FileText, + Puzzle, + Gear, + CaretRight, + History +} from 'phosphor-react'; + +interface SideNavItemProps { + icon: React.ReactNode; + label: string; + path: string; + isActive?: boolean; + onClick?: () => void; +} + +const SideNavItem: React.FC = ({ + icon, + label, + path, + isActive = false, + onClick +}) => { + return ( +
  • + { + e.preventDefault(); + if (onClick) onClick(); + }} + className={cn( + "flex items-center gap-3 px-3 py-2 rounded-lg transition-all duration-200", + isActive + ? "bg-white/10 text-white" + : "text-white/70 hover:text-white hover:bg-white/5" + )} + > +
    + {icon} +
    + {label} + {isActive && ( + + )} +
    +
  • + ); +}; + +interface SideNavSectionProps { + title?: string; + children: React.ReactNode; +} + +const SideNavSection: React.FC = ({ title, children }) => { + return ( +
    + {title && ( +

    + {title} +

    + )} +
      {children}
    +
    + ); +}; + +const SideNavDivider: React.FC = () => { + return
    ; +}; + +export const CustomSideNav: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const currentPath = location.pathname; + + const handleNavigation = (path: string) => { + navigate(path); + }; + + return ( +
    + {/* Logo Area */} +
    +
    + G +
    + Goose +
    + + {/* Navigation */} +
    + + } + label="Home" + path="/" + isActive={currentPath === '/'} + onClick={() => handleNavigation('/')} + /> + } + label="Chat" + path="/pair" + isActive={currentPath === '/pair'} + onClick={() => handleNavigation('/pair')} + /> + + + + + + } + label="Sessions" + path="/sessions" + isActive={currentPath === '/sessions'} + onClick={() => handleNavigation('/sessions')} + /> + } + label="Scheduler" + path="/schedules" + isActive={currentPath === '/schedules'} + onClick={() => handleNavigation('/schedules')} + /> + + + + + + } + label="Recipes" + path="/recipes" + isActive={currentPath === '/recipes'} + onClick={() => handleNavigation('/recipes')} + /> + } + label="Extensions" + path="/extensions" + isActive={currentPath === '/extensions'} + onClick={() => handleNavigation('/extensions')} + /> + +
    + + {/* Footer */} +
    + } + label="Settings" + path="/settings" + isActive={currentPath === '/settings'} + onClick={() => handleNavigation('/settings')} + /> +
    +
    + ); +}; + +export default CustomSideNav; diff --git a/ui/desktop/src/components/GooseMessage.tsx b/ui/desktop/src/components/GooseMessage.tsx index 31bc9ae1b5e1..8aa4c30e297b 100644 --- a/ui/desktop/src/components/GooseMessage.tsx +++ b/ui/desktop/src/components/GooseMessage.tsx @@ -146,8 +146,8 @@ export default function GooseMessage({ ]); return ( -
    -
    +
    +
    {/* Chain-of-Thought (hidden by default) */} {cotText && (
    diff --git a/ui/desktop/src/components/PillSideNav.tsx b/ui/desktop/src/components/PillSideNav.tsx index a9d046bcbed3..bb8f9cf151be 100644 --- a/ui/desktop/src/components/PillSideNav.tsx +++ b/ui/desktop/src/components/PillSideNav.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { cn } from '../utils'; import { @@ -13,49 +13,32 @@ import { X as CloseIcon } from 'lucide-react'; -interface NavItemProps { - icon: React.ReactNode; - label: string; - path: string; - isActive?: boolean; - onClick: () => void; -} - -const NavItem: React.FC = ({ - icon, - label, - isActive = false, - onClick -}) => { - return ( - - ); -}; - export const PillSideNav: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); const currentPath = location.pathname; const [isExpanded, setIsExpanded] = useState(false); + const navRef = useRef(null); const handleNavigation = (path: string) => { navigate(path); setIsExpanded(false); }; + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (navRef.current && !navRef.current.contains(event.target as Node)) { + setIsExpanded(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + // Navigation items configuration const navItems = [ { icon: , label: 'Home', path: '/' }, @@ -67,58 +50,57 @@ export const PillSideNav: React.FC = () => { // Find the current active item const activeItem = navItems.find(item => item.path === currentPath) || navItems[0]; - - // Get current mode label - const currentModeLabel = activeItem.label; + // Create a reordered list with the active item first + const orderedNavItems = [ + activeItem, + ...navItems.filter(item => item.path !== activeItem.path) + ]; + return ( -
    - {/* Collapsed Pill */} - {!isExpanded && ( -
    setIsExpanded(true)} - > - {currentModeLabel} -
    - )} - - {/* Expanded Navigation */} - {isExpanded && ( -
    -
    - {/* Header with close button */} -
    - Navigation - +
    + {/* All Pills (including the active one) */} +
    + {orderedNavItems.map((item, index) => { + const isActive = item.path === currentPath; + const showPill = isExpanded || isActive; + + // Calculate position - first pill is at top (0), others follow + const verticalOffset = index * 42; // Height of pill + small gap + + return ( +
    { + if (isActive) { + setIsExpanded(!isExpanded); + } else { + handleNavigation(item.path); + } + }} + > +
    +
    {item.icon}
    + {item.label} +
    - - {/* Navigation Items */} -
    - {navItems.map((item) => ( - handleNavigation(item.path)} - /> - ))} -
    -
    -
    - )} + ); + })} +
    ); }; diff --git a/ui/desktop/src/components/UserMessage.tsx b/ui/desktop/src/components/UserMessage.tsx index 4eecb8016d75..bf3c8fdcdc46 100644 --- a/ui/desktop/src/components/UserMessage.tsx +++ b/ui/desktop/src/components/UserMessage.tsx @@ -31,8 +31,8 @@ export default function UserMessage({ message }: UserMessageProps) { const urls = extractUrls(displayText, []); return ( -
    -
    +
    +
    diff --git a/ui/desktop/src/components/pair.tsx b/ui/desktop/src/components/pair.tsx index 8172d5388cac..6554c6ddb6c5 100644 --- a/ui/desktop/src/components/pair.tsx +++ b/ui/desktop/src/components/pair.tsx @@ -56,6 +56,7 @@ export default function Pair({ const [shouldAutoSubmit, setShouldAutoSubmit] = useState(false); const [initialMessage, setInitialMessage] = useState(null); const [isTransitioningFromHub, setIsTransitioningFromHub] = useState(false); + const [isInFocusMode, setIsInFocusMode] = useState(false); // Get recipe configuration and parameter handling const { initialPrompt: recipeInitialPrompt } = useRecipeManager(chat.messages, location.state); @@ -64,6 +65,9 @@ export default function Pair({ const { state: currentSidebarState } = useSidebar(); const isSidebarCollapsed = currentSidebarState === 'collapsed'; + // Get system theme preference + const prefersDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + // Override backgrounds to allow our gradient to show through useEffect(() => { // Target the specific SidebarInset component with the complex class @@ -123,9 +127,6 @@ export default function Pair({ const chatInputContainer = document.querySelector('[data-drop-zone="true"]') as HTMLElement; if (chatInputContainer) { chatInputContainer.style.background = 'rgba(255, 255, 255, 0.05)'; - chatInputContainer.style.backdropFilter = 'blur(10px)'; - // @ts-expect-error - webkitBackdropFilter is a valid CSS property - chatInputContainer.style.webkitBackdropFilter = 'blur(10px)'; chatInputContainer.style.border = '1px solid rgba(255, 255, 255, 0.1)'; } @@ -166,9 +167,6 @@ export default function Pair({ }); if (chatInputContainer) { chatInputContainer.style.background = ''; - chatInputContainer.style.backdropFilter = ''; - // @ts-expect-error - webkitBackdropFilter is a valid CSS property - chatInputContainer.style.webkitBackdropFilter = ''; chatInputContainer.style.border = ''; } }; @@ -282,6 +280,7 @@ export default function Pair({ // This is called after a message is submitted setShouldAutoSubmit(false); setIsTransitioningFromHub(false); // Clear transitioning state once message is submitted + setIsInFocusMode(true); // Enable focus mode when user sends a message console.log('Message submitted:', message); }; @@ -289,6 +288,7 @@ export default function Pair({ const handleMessageStreamFinish = () => { // This will be called with the proper append function from BaseChat // For now, we'll handle auto-execution in the BaseChat component + // Focus mode remains active until chat is refreshed }; // Determine the initial value for the chat input @@ -315,6 +315,17 @@ export default function Pair({ return
    {/* Any Pair-specific content before messages can go here */}
    ; }; + // Fixed blur intensity and background color based on theme - matching home page styling + const blurIntensity = 20; // Consistent blur for chat mode + + // Get the actual theme from document.documentElement + const isDarkTheme = document.documentElement.classList.contains('dark'); + + // Determine background color based on focus mode and theme + const backgroundColor = isInFocusMode + ? (isDarkTheme ? 'rgba(0, 0, 0, 0.9)' : 'rgba(255, 255, 255, 0.9)') // 90% opacity in focus mode + : (isDarkTheme ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)'); // 70% opacity in normal mode + return (
    {/* Image background implementation */} @@ -327,9 +338,13 @@ export default function Pair({ }} /> - {/* Optional overlay for better text readability */} + {/* Fixed blur overlay - always present with consistent intensity */}
    {/* Centered chat content */}