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}
+
+ )}
+
+
+ );
+};
+
+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 */}
+
+
+ {/* 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 */}