iteration

This commit is contained in:
Nahiyan Khan
2025-06-27 05:58:35 -04:00
parent 22f9dab6aa
commit 3f08987a02
10 changed files with 268 additions and 82 deletions
+57 -26
View File
@@ -4,9 +4,15 @@ import type { View } from '../App';
import Stop from './ui/Stop';
import { Attach, Send, Close } from './icons';
import { debounce } from 'lodash';
import BottomMenu from './bottom_menu/BottomMenu';
import { LocalMessageStorage } from '../utils/localMessageStorage';
import { Message } from '../types/message';
import { DirSwitcher } from './bottom_menu/DirSwitcher';
import BottomMenuAlertPopover from './bottom_menu/BottomMenuAlertPopover';
import ModelsBottomBar from './settings/models/bottom_bar/ModelsBottomBar';
import { BottomMenuModeSelection } from './bottom_menu/BottomMenuModeSelection';
import { ManualSummarizeButton } from './context_management/ManualSummaryButton';
import { useAlerts } from './alerts';
import { useToolCount } from './alerts/useToolCount';
interface PastedImage {
id: string;
@@ -41,11 +47,19 @@ export default function ChatInput({
commandHistory = [],
initialValue = '',
droppedFiles = [],
setView,
numTokens,
hasMessages,
messages = [],
setMessages,
}: ChatInputProps) {
const [_value, setValue] = useState(initialValue);
const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback
const [isFocused, setIsFocused] = useState(false);
const [pastedImages, setPastedImages] = useState<PastedImage[]>([]);
const { alerts, addAlert, clearAlerts } = useAlerts();
const dropdownRef = useRef<HTMLDivElement>(null);
const toolCount = useToolCount();
// Update internal value when initialValue changes
useEffect(() => {
@@ -434,7 +448,11 @@ export default function ChatInput({
: 'border-borderSubtle hover:border-borderStandard'
} z-10`}
>
<form onSubmit={onFormSubmit}>
{/* DirSwitcher at the top */}
<div className="p-2 pb-0">
<DirSwitcher hasMessages={messages.length > 0} />
</div>
<form onSubmit={onFormSubmit} className="flex flex-col">
<textarea
data-testid="chat-input"
autoFocus
@@ -455,38 +473,16 @@ export default function ChatInput({
maxHeight: `${maxHeight}px`,
overflowY: 'auto',
}}
className="w-full pl-4 pr-[68px] outline-none border-none focus:ring-0 bg-transparent pt-3 pb-1.5 text-sm resize-none text-textStandard placeholder:text-textPlaceholder"
className="w-full outline-none border-none focus:ring-0 bg-transparent px-3 pt-3 pb-1.5 text-sm resize-none text-textStandard placeholder:text-textPlaceholder"
/>
<div className="flex items-center gap-1 absolute right-2 top-1/2 -translate-y-1/2">
<Button type="button" shape="round" size="sm" variant="ghost" onClick={handleFileSelect}>
<Attach />
</Button>
{isLoading ? (
<Button type="button" onClick={onStop} shape="round" size="sm" variant="ghost">
<Stop />
</Button>
) : (
<Button
type="submit"
size="sm"
variant="ghost"
shape="round"
// disabled={!displayValue.trim() && pastedImages.length === 0}
>
<Send />
</Button>
)}
</div>
{pastedImages.length > 0 && (
<div className="flex flex-wrap gap-2 p-2 border-t border-borderSubtle">
{pastedImages.map((img) => (
<div key={img.id} className="relative group w-20 h-20">
{img.dataUrl && (
<img
src={img.dataUrl} // Use dataUrl for instant preview
src={img.dataUrl}
alt={`Pasted image ${img.id}`}
className={`w-full h-full object-cover rounded border ${img.error ? 'border-red-500' : 'border-borderStandard'}`}
/>
@@ -506,6 +502,8 @@ export default function ChatInput({
type="button"
onClick={() => handleRetryImageSave(img.id)}
title="Retry saving image"
variant="outline"
size="xs"
>
Retry
</Button>
@@ -519,6 +517,8 @@ export default function ChatInput({
onClick={() => handleRemovePastedImage(img.id)}
className="absolute -top-1 -right-1 opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity z-10"
aria-label="Remove image"
variant="outline"
size="xs"
>
<Close />
</Button>
@@ -527,6 +527,37 @@ export default function ChatInput({
))}
</div>
)}
{/* Actions and model/mode/alerts row below input */}
<div className="flex flex-row items-center gap-1 p-2">
{/* Send/Attach/Stop actions */}
<Button type="button" size="xs" variant="outline" onClick={handleFileSelect}>
<Attach />
</Button>
{isLoading ? (
<Button type="button" onClick={onStop} size="xs" variant="outline">
<Stop />
</Button>
) : (
<Button type="submit" size="xs" variant="outline">
<Send />
</Button>
)}
{/* Model selector, mode selector, alerts, summarize button */}
<div className="flex flex-row items-center gap-2 ml-4">
<ModelsBottomBar dropdownRef={dropdownRef} setView={setView} />
<BottomMenuModeSelection setView={setView} />
<BottomMenuAlertPopover alerts={alerts} />
{messages.length > 0 && (
<ManualSummarizeButton
messages={messages}
isLoading={isLoading}
setMessages={setMessages}
/>
)}
</div>
</div>
</form>
</div>
);
+5 -9
View File
@@ -43,7 +43,6 @@ import {
} from '../types/message';
import SessionsSidebar from './GooseSidebar/AppSidebar';
import { SidebarTrigger } from './ui/sidebar';
import BottomMenu from './bottom_menu/BottomMenu';
import { SessionInsights } from './sessions/SessionsInsights';
import { useSidebar } from './ui/sidebar';
import { Button } from './ui/button';
@@ -418,6 +417,11 @@ function ChatContentWithSidebar({
// Handle stopping the message stream
const lastMessage = messages[messages.length - 1];
// Check if there are any messages before proceeding
if (!lastMessage) {
return;
}
// check if the last user message has any tool response(s)
const isToolResponse = lastMessage.content.some(
(content): content is ToolResponseMessageContent => content.type == 'toolResponse'
@@ -847,14 +851,6 @@ function ChatContentWithSidebar({
</div>
</MainPanelLayout>
<BottomMenu
setView={setView}
numTokens={sessionTokenCount}
messages={messages}
isLoading={isLoading}
setMessages={setMessages}
/>
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
<SessionSummaryModal
@@ -1,5 +1,14 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Folder, FileText, Clock, MessageCircle, Home, Loader2 } from 'lucide-react';
import {
Folder,
FileText,
Clock,
MessageCircle,
Home,
Loader2,
AppWindowMac,
AppWindow,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import {
@@ -204,7 +213,11 @@ const AppSidebar: React.FC<SidebarProps> = ({ setIsGoosehintsModalOpen, setView,
tooltip="Start a new session in a new window"
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
>
<ChatSmart className="w-4 h-4" />
{safeIsMacOS ? (
<AppWindowMac className="w-4 h-4" />
) : (
<AppWindow className="w-4 h-4" />
)}
<span>New window</span>
</SidebarMenuButton>
</SidebarMenuItem>
@@ -2,7 +2,7 @@ import React from 'react';
export const MainPanelLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div className="flex flex-col flex-1 min-w-0 h-[calc(100dvh-80px)] shadow-default bg-background-default mr-2 mb-2 rounded-2xl animate-in fade-in slide-in-from-right-8 duration-500">
<div className="flex flex-col flex-1 min-w-0 h-[calc(100dvh-56px)] shadow-default bg-background-default mr-2 mb-2 rounded-2xl animate-in fade-in slide-in-from-right-8 duration-500">
{children}
</div>
);
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { Document } from '../icons';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/Tooltip';
import { Button } from '../ui/button';
interface DirSwitcherProps {
hasMessages?: boolean;
@@ -25,15 +26,17 @@ export const DirSwitcher: React.FC<DirSwitcherProps> = ({
<TooltipProvider>
<Tooltip open={isTooltipOpen} onOpenChange={setIsTooltipOpen}>
<TooltipTrigger asChild>
<button
<Button
className={`z-[100] hover:cursor-pointer text-text-default/70 hover:text-text-default text-xs flex items-center transition-colors pl-1 [&>svg]:size-4 ${className}`}
onClick={handleDirectoryChange}
variant="outline"
size="xs"
>
<Document className="mr-1" />
<div className="max-w-[200px] truncate [direction:rtl]">
{String(window.appConfig.get('GOOSE_WORKING_DIR'))}
</div>
</button>
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-96 overflow-auto scrollbar-thin" side="top">
{window.appConfig.get('GOOSE_WORKING_DIR') as string}
+7 -5
View File
@@ -419,6 +419,11 @@ function HubContentWithSidebar({
// Handle stopping the message stream
const lastMessage = messages[messages.length - 1];
// Check if there are any messages before proceeding
if (!lastMessage) {
return;
}
// check if the last user message has any tool response(s)
const isToolResponse = lastMessage.content.some(
(content): content is ToolResponseMessageContent => content.type == 'toolResponse'
@@ -585,10 +590,7 @@ function HubContentWithSidebar({
{/* Loader when generating recipe */}
{isGeneratingRecipe && <LayingEggLoader />}
<div className="h-12 flex items-center justify-between">
{/* <div className={`flex items-center ${headerPadding}`}>
<SidebarTrigger className="no-drag" />
</div> */}
<div className="h-12 flex items-center justify-between absolute">
<div className="flex items-center pr-4">
{messages.length > 0 && (
<>
@@ -693,7 +695,7 @@ function HubContentWithSidebar({
</div>
<div
className="flex flex-col min-w-0 flex-1 overflow-y-scroll relative pl-6 pr-4 pb-16 pt-2"
className="flex flex-col min-w-0 flex-1 overflow-y-scroll relative pl-6 pr-4 pb-16"
onDrop={handleDrop}
onDragOver={handleDragOver}
>
+5 -13
View File
@@ -417,6 +417,11 @@ function PairContentWithSidebar({
// Handle stopping the message stream
const lastMessage = messages[messages.length - 1];
// Check if there are any messages before proceeding
if (!lastMessage) {
return;
}
// check if the last user message has any tool response(s)
const isToolResponse = lastMessage.content.some(
(content): content is ToolResponseMessageContent => content.type == 'toolResponse'
@@ -584,9 +589,6 @@ function PairContentWithSidebar({
{isGeneratingRecipe && <LayingEggLoader />}
<div className="h-12 flex items-center justify-between">
<div className={`flex items-center ${headerPadding}`}>
<SidebarTrigger className="no-drag" />
</div>
<div className="flex items-center pr-4">
{messages.length > 0 && (
<>
@@ -811,16 +813,6 @@ function PairContentWithSidebar({
</div>
</MainPanelLayout>
<BottomMenu
setView={setView}
numTokens={sessionTokenCount}
messages={messages}
isLoading={isLoading}
setMessages={setMessages}
/>
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
<SessionSummaryModal
isOpen={isSummaryModalOpen}
onClose={closeSummaryModal}
@@ -1,5 +1,6 @@
import { ScrollArea } from '../ui/scroll-area';
import { SidebarTrigger, useSidebar } from '../ui/sidebar';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/vertical-tabs';
import type { View, ViewOptions } from '../../App';
import ExtensionsSection from './extensions/ExtensionsSection';
import ModelsSection from './models/ModelsSection';
@@ -11,6 +12,8 @@ import AppSettingsSection from './app/AppSettingsSection';
import { ExtensionConfig } from '../../api';
import MoreMenuLayout from '../more_menu/MoreMenuLayout';
import { MainPanelLayout } from '../Layout/MainPanelLayout';
import { Bot, Puzzle, Settings, Share2, Palette, Wrench, Monitor, Target } from 'lucide-react';
import { useState, useEffect } from 'react';
export type SettingsViewOptions = {
deepLinkConfig?: ExtensionConfig;
@@ -28,12 +31,35 @@ export default function SettingsView({
viewOptions: SettingsViewOptions;
}) {
const { open: isSidebarOpen } = useSidebar();
const [activeTab, setActiveTab] = useState('models');
const safeIsMacOS = (window?.electron?.platform || 'darwin') === 'darwin';
// Calculate padding based on sidebar state and macOS
const headerPadding = !isSidebarOpen ? (safeIsMacOS ? 'pl-20' : 'pl-12') : 'pl-4';
// Determine initial tab based on section prop
useEffect(() => {
if (viewOptions.section) {
// Map section names to tab values
const sectionToTab: Record<string, string> = {
update: 'app',
models: 'models',
extensions: 'extensions',
modes: 'modes',
sharing: 'sharing',
styles: 'styles',
tools: 'tools',
app: 'app',
};
const targetTab = sectionToTab[viewOptions.section];
if (targetTab) {
setActiveTab(targetTab);
}
}
}, [viewOptions.section]);
return (
<>
<MainPanelLayout>
@@ -53,29 +79,73 @@ export default function SettingsView({
</div>
<div className="flex-1 min-h-0 relative px-6">
<ScrollArea className="h-full">
<div className="h-full relative">
<div className="space-y-8 pt-4">
{/* Models Section */}
<ModelsSection setView={setView} />
{/* Extensions Section */}
<ExtensionsSection
deepLinkConfig={viewOptions.deepLinkConfig}
showEnvVars={viewOptions.showEnvVars}
/>
{/* Goose Modes */}
<ModeSection setView={setView} />
{/*Session sharing*/}
<SessionSharingSection />
{/* Response Styles */}
<ResponseStylesSection />
{/* Tool Selection Strategy */}
<ToolSelectionStrategySection setView={setView} />
{/* App Settings */}
<AppSettingsSection scrollToSection={viewOptions.section} />
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex">
<TabsList className="w-48 h-full">
<TabsTrigger value="models" className="flex gap-2">
<Bot className="h-4 w-4" />
Models
</TabsTrigger>
<TabsTrigger value="extensions" className="flex gap-2">
<Puzzle className="h-4 w-4" />
Extensions
</TabsTrigger>
<TabsTrigger value="modes" className="flex gap-2">
<Wrench className="h-4 w-4" />
Modes
</TabsTrigger>
<TabsTrigger value="sharing" className="flex gap-2">
<Share2 className="h-4 w-4" />
Session Sharing
</TabsTrigger>
<TabsTrigger value="styles" className="flex gap-2">
<Palette className="h-4 w-4" />
Response Styles
</TabsTrigger>
<TabsTrigger value="tools" className="flex gap-2">
<Target className="h-4 w-4" />
Tool Selection
</TabsTrigger>
<TabsTrigger value="app" className="flex gap-2">
<Monitor className="h-4 w-4" />
App Settings
</TabsTrigger>
</TabsList>
<div className="flex-1 ml-6">
<ScrollArea className="h-full">
<TabsContent value="models" className="mt-0">
<ModelsSection setView={setView} />
</TabsContent>
<TabsContent value="extensions" className="mt-0">
<ExtensionsSection
deepLinkConfig={viewOptions.deepLinkConfig}
showEnvVars={viewOptions.showEnvVars}
/>
</TabsContent>
<TabsContent value="modes" className="mt-0">
<ModeSection setView={setView} />
</TabsContent>
<TabsContent value="sharing" className="mt-0">
<SessionSharingSection />
</TabsContent>
<TabsContent value="styles" className="mt-0">
<ResponseStylesSection />
</TabsContent>
<TabsContent value="tools" className="mt-0">
<ToolSelectionStrategySection setView={setView} />
</TabsContent>
<TabsContent value="app" className="mt-0">
<AppSettingsSection scrollToSection={viewOptions.section} />
</TabsContent>
</ScrollArea>
</div>
</ScrollArea>
</Tabs>
</div>
</div>
</MainPanelLayout>
+13 -2
View File
@@ -11,22 +11,28 @@ const buttonVariants = cva(
default: 'bg-background-accent text-text-on-accent hover:bg-background-accent/90 shadow-xs',
destructive:
'bg-background-danger text-white hover:bg-background-danger/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-background-danger/60 shadow-xs',
outline: 'border hover:bg-background-default hover:shadow-default hover:border-transparent',
outline: 'border hover:bg-background-muted',
secondary: 'bg-background-muted text-text-default hover:bg-background-muted/80 shadow-xs',
ghost: 'hover:bg-background-muted dark:hover:bg-background-muted/50',
link: 'text-text-accent underline-offset-4 hover:underline',
},
size: {
xs: 'h-6 gap-1 ![&_svg:not([class*="size-"])]:size-3',
default: 'h-9',
sm: 'h-8 gap-1.5',
lg: 'h-10',
},
shape: {
pill: 'rounded-full',
pill: 'rounded-lg',
round: '',
},
},
compoundVariants: [
{
shape: 'pill',
size: 'xs',
className: 'px-2 has-[>svg]:px-2',
},
{
shape: 'pill',
size: 'default',
@@ -42,6 +48,11 @@ const buttonVariants = cva(
size: 'lg',
className: 'px-6 has-[>svg]:px-6',
},
{
shape: 'round',
size: 'xs',
className: 'w-6 h-6 p-0 rounded-full',
},
{
shape: 'round',
size: 'default',
@@ -0,0 +1,68 @@
'use client';
import * as React from 'react';
import { cn } from '../../utils';
import * as TabsPrimitive from '@radix-ui/react-tabs';
const Tabs = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Root
orientation="vertical"
ref={ref}
className={cn('flex rounded-md text-text-muted gap-1', className)}
{...props}
/>
));
Tabs.displayName = TabsPrimitive.List.displayName;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'flex flex-col h-auto justify-start rounded-md bg-background-default p-1 text-muted-foreground',
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'flex items-center justify-start whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'ml-4 mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsContent, TabsList, TabsTrigger };