Skip to content
59 changes: 28 additions & 31 deletions ui/desktop/src/components/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -103,6 +105,7 @@ function BaseChatContent({
enableLocalStorage = false,
onMessageStreamFinish,
onMessageSubmit,
onTypingStateChange,
renderHeader,
renderBeforeMessages,
renderAfterMessages,
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -541,28 +536,30 @@ function BaseChatContent({
<div
className={`relative z-10 ${disableAnimation ? '' : 'animate-[fadein_400ms_ease-in_forwards]'}`}
>
<ChatInput
handleSubmit={handleSubmit}
chatState={chatState}
onStop={onStopGoose}
commandHistory={commandHistory}
initialValue={input || ''}
setView={setView}
numTokens={sessionTokenCount}
inputTokens={sessionInputTokens || localInputTokens}
outputTokens={sessionOutputTokens || localOutputTokens}
droppedFiles={droppedFiles}
onFilesProcessed={() => setDroppedFiles([])} // Clear dropped files after processing
messages={messages}
setMessages={setMessages}
disableAnimation={disableAnimation}
sessionCosts={sessionCosts}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
recipeConfig={recipeConfig}
recipeAccepted={recipeAccepted}
initialPrompt={initialPrompt}
{...customChatInputProps}
/>
<ChatInputWrapper onTypingStateChange={onTypingStateChange}>
<ChatInput
handleSubmit={handleSubmit}
chatState={chatState}
onStop={onStopGoose}
commandHistory={commandHistory}
initialValue={input || ''}
setView={setView}
numTokens={sessionTokenCount}
inputTokens={sessionInputTokens || localInputTokens}
outputTokens={sessionOutputTokens || localOutputTokens}
droppedFiles={droppedFiles}
onFilesProcessed={() => setDroppedFiles([])} // Clear dropped files after processing
messages={messages}
setMessages={setMessages}
disableAnimation={disableAnimation}
sessionCosts={sessionCosts}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
recipeConfig={recipeConfig}
recipeAccepted={recipeAccepted}
initialPrompt={initialPrompt}
{...customChatInputProps}
/>
</ChatInputWrapper>
</div>
</MainPanelLayout>

Expand Down
56 changes: 56 additions & 0 deletions ui/desktop/src/components/BlurOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useState, useEffect } from 'react';

interface BlurOverlayProps {
isActive: boolean;
isDarkMode?: boolean;
intensity?: number;
}

const BlurOverlay: React.FC<BlurOverlayProps> = ({
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 (
<div
className="fixed inset-0 pointer-events-none z-40 transition-all duration-300 ease-in-out"
style={{
backdropFilter: `blur(${opacity * intensity}px)`,
backgroundColor: isDarkMode
? `rgba(0, 0, 0, ${opacity * 0.7})`
: `rgba(255, 255, 255, ${opacity * 0.7})`,
opacity: opacity
}}
/>
);
};

export default BlurOverlay;
54 changes: 54 additions & 0 deletions ui/desktop/src/components/ChatInputWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useState, useEffect, useRef } from 'react';

interface ChatInputWrapperProps {
children: React.ReactNode;
onTypingStateChange?: (isTyping: boolean) => void;
}

const ChatInputWrapper: React.FC<ChatInputWrapperProps> = ({ children, onTypingStateChange }) => {
const [isTyping, setIsTyping] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(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 (
<div className="relative">
{/* Render the chat input */}
{children}
</div>
);
};

export default ChatInputWrapper;
170 changes: 170 additions & 0 deletions ui/desktop/src/components/CustomSideNav.tsx
Original file line number Diff line number Diff line change
@@ -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<SideNavItemProps> = ({
icon,
label,
path,
isActive = false,
onClick
}) => {
return (
<li className="mb-1">
<a
href={path}
onClick={(e) => {
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"
)}
>
<div className="text-lg">
{icon}
</div>
<span className="text-sm font-medium">{label}</span>
{isActive && (
<CaretRight weight="bold" className="ml-auto text-white/70" size={16} />
)}
</a>
</li>
);
};

interface SideNavSectionProps {
title?: string;
children: React.ReactNode;
}

const SideNavSection: React.FC<SideNavSectionProps> = ({ title, children }) => {
return (
<div className="mb-6">
{title && (
<h3 className="text-xs font-medium text-white/50 uppercase tracking-wider px-3 mb-2">
{title}
</h3>
)}
<ul>{children}</ul>
</div>
);
};

const SideNavDivider: React.FC = () => {
return <div className="h-px bg-white/10 my-4 mx-3" />;
};

export const CustomSideNav: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const currentPath = location.pathname;

const handleNavigation = (path: string) => {
navigate(path);
};

return (
<div className="h-full w-64 bg-gradient-to-b from-gray-800/90 to-gray-900/90 backdrop-blur-lg border-r border-white/10 flex flex-col">
{/* Logo Area */}
<div className="flex items-center px-4 py-6">
<div className="w-8 h-8 bg-white rounded-full flex items-center justify-center">
<span className="text-gray-900 font-bold text-lg">G</span>
</div>
<span className="ml-2 text-white font-medium text-lg">Goose</span>
</div>

{/* Navigation */}
<div className="flex-1 overflow-y-auto px-3 py-2">
<SideNavSection>
<SideNavItem
icon={<House weight="fill" />}
label="Home"
path="/"
isActive={currentPath === '/'}
onClick={() => handleNavigation('/')}
/>
<SideNavItem
icon={<ChatCenteredText weight="fill" />}
label="Chat"
path="/pair"
isActive={currentPath === '/pair'}
onClick={() => handleNavigation('/pair')}
/>
</SideNavSection>

<SideNavDivider />

<SideNavSection title="History">
<SideNavItem
icon={<History weight="fill" />}
label="Sessions"
path="/sessions"
isActive={currentPath === '/sessions'}
onClick={() => handleNavigation('/sessions')}
/>
<SideNavItem
icon={<Clock weight="fill" />}
label="Scheduler"
path="/schedules"
isActive={currentPath === '/schedules'}
onClick={() => handleNavigation('/schedules')}
/>
</SideNavSection>

<SideNavDivider />

<SideNavSection title="Tools">
<SideNavItem
icon={<FileText weight="fill" />}
label="Recipes"
path="/recipes"
isActive={currentPath === '/recipes'}
onClick={() => handleNavigation('/recipes')}
/>
<SideNavItem
icon={<Puzzle weight="fill" />}
label="Extensions"
path="/extensions"
isActive={currentPath === '/extensions'}
onClick={() => handleNavigation('/extensions')}
/>
</SideNavSection>
</div>

{/* Footer */}
<div className="p-3 border-t border-white/10">
<SideNavItem
icon={<Gear weight="fill" />}
label="Settings"
path="/settings"
isActive={currentPath === '/settings'}
onClick={() => handleNavigation('/settings')}
/>
</div>
</div>
);
};

export default CustomSideNav;
4 changes: 2 additions & 2 deletions ui/desktop/src/components/GooseMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ export default function GooseMessage({
]);

return (
<div className="goose-message flex w-[90%] justify-start min-w-0">
<div className="flex flex-col w-full min-w-0">
<div className="goose-message flex w-[90%] justify-start min-w-0 bg-transparent">
<div className="flex flex-col w-full min-w-0 bg-transparent">
{/* Chain-of-Thought (hidden by default) */}
{cotText && (
<details className="bg-bgSubtle border border-borderSubtle rounded p-2 mb-2">
Expand Down
Loading