Added compact mode for more concise answers.

Made optimization mode persist between page refreshes.
Added mode switcher to chat so it can be changed while researching.
This commit is contained in:
Willie Zutz
2025-02-16 15:02:05 -07:00
parent 18b6f5b674
commit 467905d9f2
12 changed files with 255 additions and 53 deletions

View File

@ -16,9 +16,17 @@ const Chat = ({
setFileIds,
files,
setFiles,
isCompact,
setIsCompact,
optimizationMode,
setOptimizationMode,
}: {
messages: Message[];
sendMessage: (message: string) => void;
sendMessage: (
message: string,
messageId?: string,
options?: { isCompact?: boolean },
) => void;
loading: boolean;
messageAppeared: boolean;
rewrite: (messageId: string) => void;
@ -26,6 +34,10 @@ const Chat = ({
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
isCompact: boolean;
setIsCompact: (isCompact: boolean) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
}) => {
const [dividerWidth, setDividerWidth] = useState(0);
const dividerRef = useRef<HTMLDivElement | null>(null);
@ -71,6 +83,7 @@ const Chat = ({
dividerRef={isLast ? dividerRef : undefined}
isLast={isLast}
rewrite={rewrite}
isCompact={isCompact}
sendMessage={sendMessage}
/>
{!isLast && msg.role === 'assistant' && (
@ -83,7 +96,7 @@ const Chat = ({
<div ref={messageEnd} className="h-0" />
{dividerWidth > 0 && (
<div
className="bottom-24 lg:bottom-10 fixed z-40"
className="bottom-24 lg:bottom-10 fixed"
style={{ width: dividerWidth }}
>
<MessageInput
@ -93,6 +106,10 @@ const Chat = ({
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
isCompact={isCompact}
setIsCompact={setIsCompact}
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
</div>
)}

View File

@ -399,6 +399,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
const [focusMode, setFocusMode] = useState('webSearch');
const [optimizationMode, setOptimizationMode] = useState('speed');
const [isCompact, setIsCompact] = useState(false);
const [isMessagesLoaded, setIsMessagesLoaded] = useState(false);
@ -406,6 +407,21 @@ const ChatWindow = ({ id }: { id?: string }) => {
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
useEffect(() => {
const savedCompactMode = localStorage.getItem('compactMode');
const savedOptimizationMode = localStorage.getItem('optimizationMode');
if (savedCompactMode !== null) {
setIsCompact(savedCompactMode === 'true');
}
if (savedOptimizationMode !== null) {
setOptimizationMode(savedOptimizationMode);
} else {
localStorage.setItem('optimizationMode', optimizationMode);
}
}, []);
useEffect(() => {
if (
chatId &&
@ -456,7 +472,11 @@ const ChatWindow = ({ id }: { id?: string }) => {
}
}, [isMessagesLoaded, isWSReady]);
const sendMessage = async (message: string, messageId?: string) => {
const sendMessage = async (
message: string,
messageId?: string,
options?: { isCompact?: boolean },
) => {
if (loading) return;
if (!ws || ws.readyState !== WebSocket.OPEN) {
toast.error('Cannot send message while disconnected');
@ -471,21 +491,21 @@ const ChatWindow = ({ id }: { id?: string }) => {
let added = false;
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
ws.send(
JSON.stringify({
type: 'message',
message: {
messageId: messageId,
chatId: chatId!,
content: message,
},
files: fileIds,
focusMode: focusMode,
optimizationMode: optimizationMode,
history: [...chatHistory, ['human', message]],
}),
);
let messageData = {
type: 'message',
message: {
messageId: messageId,
chatId: chatId!,
content: message,
},
files: fileIds,
focusMode: focusMode,
optimizationMode: optimizationMode,
history: [...chatHistory, ['human', message]],
isCompact: options?.isCompact ?? isCompact,
};
//console.log('sending:', messageData, JSON.stringify(messageData));
ws.send(JSON.stringify(messageData));
setMessages((prevMessages) => [
...prevMessages,
@ -615,12 +635,12 @@ const ChatWindow = ({ id }: { id?: string }) => {
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
});
sendMessage(message.content, message.messageId);
sendMessage(message.content, message.messageId, { isCompact });
};
useEffect(() => {
if (isReady && initialMessage && ws?.readyState === 1) {
sendMessage(initialMessage);
sendMessage(initialMessage, undefined, { isCompact });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ws?.readyState, isReady, initialMessage, isWSReady]);
@ -660,6 +680,10 @@ const ChatWindow = ({ id }: { id?: string }) => {
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
isCompact={isCompact}
setIsCompact={setIsCompact}
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
</>
) : (
@ -673,6 +697,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
isCompact={isCompact}
setIsCompact={setIsCompact}
/>
)}
</div>

View File

@ -14,6 +14,8 @@ const EmptyChat = ({
setFileIds,
files,
setFiles,
isCompact,
setIsCompact,
}: {
sendMessage: (message: string) => void;
focusMode: string;
@ -24,6 +26,8 @@ const EmptyChat = ({
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
isCompact: boolean;
setIsCompact: (isCompact: boolean) => void;
}) => {
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
@ -48,6 +52,8 @@ const EmptyChat = ({
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
isCompact={isCompact}
setIsCompact={setIsCompact}
/>
</div>
</div>

View File

@ -17,8 +17,14 @@ const EmptyChatMessageInput = ({
setFileIds,
files,
setFiles,
isCompact,
setIsCompact,
}: {
sendMessage: (message: string) => void;
sendMessage: (
message: string,
messageId?: string,
options?: { isCompact?: boolean },
) => void;
focusMode: string;
setFocusMode: (mode: string) => void;
optimizationMode: string;
@ -27,6 +33,8 @@ const EmptyChatMessageInput = ({
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
isCompact: boolean;
setIsCompact: (isCompact: boolean) => void;
}) => {
const [copilotEnabled, setCopilotEnabled] = useState(false);
const [message, setMessage] = useState('');
@ -61,13 +69,13 @@ const EmptyChatMessageInput = ({
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage(message);
sendMessage(message, undefined, { isCompact });
setMessage('');
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage(message);
sendMessage(message, undefined, { isCompact });
setMessage('');
}
}}
@ -97,6 +105,8 @@ const EmptyChatMessageInput = ({
<Optimization
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
isCompact={isCompact}
setIsCompact={setIsCompact}
/>
<button
disabled={message.trim().length === 0}

View File

@ -28,6 +28,7 @@ const MessageBox = ({
dividerRef,
isLast,
rewrite,
isCompact,
sendMessage,
}: {
message: Message;
@ -37,7 +38,12 @@ const MessageBox = ({
dividerRef?: MutableRefObject<HTMLDivElement | null>;
isLast: boolean;
rewrite: (messageId: string) => void;
sendMessage: (message: string) => void;
isCompact: boolean;
sendMessage: (
message: string,
messageId?: string,
options?: { isCompact?: boolean },
) => void;
}) => {
const [parsedMessage, setParsedMessage] = useState(message.content);
const [speechMessage, setSpeechMessage] = useState(message.content);
@ -65,6 +71,10 @@ const MessageBox = ({
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
const handleSuggestionClick = (suggestion: string) => {
sendMessage(suggestion, undefined, { isCompact });
};
return (
<div>
{message.role === 'user' && (
@ -163,7 +173,7 @@ const MessageBox = ({
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
<div
onClick={() => {
sendMessage(suggestion);
handleSuggestionClick(suggestion);
}}
className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center"
>

View File

@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import Attach from './MessageInputActions/Attach';
import CopilotToggle from './MessageInputActions/Copilot';
import Optimization from './MessageInputActions/Optimization';
import { File } from './ChatWindow';
import AttachSmall from './MessageInputActions/AttachSmall';
@ -14,13 +15,25 @@ const MessageInput = ({
setFileIds,
files,
setFiles,
isCompact,
setIsCompact,
optimizationMode,
setOptimizationMode,
}: {
sendMessage: (message: string) => void;
sendMessage: (
message: string,
messageId?: string,
options?: { isCompact?: boolean },
) => void;
loading: boolean;
fileIds: string[];
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
isCompact: boolean;
setIsCompact: (isCompact: boolean) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
}) => {
const [copilotEnabled, setCopilotEnabled] = useState(false);
const [message, setMessage] = useState('');
@ -40,20 +53,16 @@ const MessageInput = ({
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const activeElement = document.activeElement;
const isInputFocused =
activeElement?.tagName === 'INPUT' ||
activeElement?.tagName === 'TEXTAREA' ||
activeElement?.hasAttribute('contenteditable');
if (e.key === '/' && !isInputFocused) {
e.preventDefault();
inputRef.current?.focus();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
@ -64,28 +73,36 @@ const MessageInput = ({
onSubmit={(e) => {
if (loading) return;
e.preventDefault();
sendMessage(message);
sendMessage(message, undefined, { isCompact });
setMessage('');
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !loading) {
e.preventDefault();
sendMessage(message);
sendMessage(message, undefined, { isCompact });
setMessage('');
}
}}
className={cn(
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200',
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center border border-light-200 dark:border-dark-200',
mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full',
)}
>
{mode === 'single' && (
<AttachSmall
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
<div className="flex flex-row items-center space-x-2">
<AttachSmall
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
<Optimization
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
isCompact={isCompact}
setIsCompact={setIsCompact}
/>
</div>
)}
<TextareaAutosize
ref={inputRef}
@ -113,12 +130,20 @@ const MessageInput = ({
)}
{mode === 'multi' && (
<div className="flex flex-row items-center justify-between w-full pt-2">
<AttachSmall
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
<div className="flex flex-row items-center space-x-2">
<AttachSmall
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
<Optimization
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
isCompact={isCompact}
setIsCompact={setIsCompact}
/>
</div>
<div className="flex flex-row items-center space-x-4">
<CopilotToggle
copilotEnabled={copilotEnabled}

View File

@ -1,4 +1,4 @@
import { ChevronDown, Sliders, Star, Zap } from 'lucide-react';
import { ChevronDown, Minimize2, Sliders, Star, Zap } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
Popover,
@ -6,8 +6,7 @@ import {
PopoverPanel,
Transition,
} from '@headlessui/react';
import { Fragment } from 'react';
import { Fragment, useEffect } from 'react';
const OptimizationModes = [
{
key: 'speed',
@ -37,10 +36,33 @@ const OptimizationModes = [
const Optimization = ({
optimizationMode,
setOptimizationMode,
isCompact,
setIsCompact,
}: {
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
isCompact: boolean;
setIsCompact: (isCompact: boolean) => void;
}) => {
useEffect(() => {
const savedCompactMode = localStorage.getItem('compactMode');
if (savedCompactMode === null) {
localStorage.setItem('compactMode', String(isCompact));
} else {
setIsCompact(savedCompactMode === 'true');
}
}, [setIsCompact]);
const handleCompactChange = (checked: boolean) => {
setIsCompact(checked);
localStorage.setItem('compactMode', String(checked));
};
const handleOptimizationChange = (mode: string) => {
setOptimizationMode(mode);
localStorage.setItem('optimizationMode', mode);
};
return (
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
<PopoverButton
@ -70,11 +92,11 @@ const Optimization = ({
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel className="absolute z-10 w-64 md:w-[250px] right-0">
<PopoverPanel className="absolute z-10 w-64 md:w-[250px] right-0 bottom-[100%] mb-2">
<div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto">
{OptimizationModes.map((mode, i) => (
<PopoverButton
onClick={() => setOptimizationMode(mode.key)}
onClick={() => handleOptimizationChange(mode.key)}
key={i}
disabled={mode.key === 'quality'}
className={cn(
@ -94,6 +116,30 @@ const Optimization = ({
</p>
</PopoverButton>
))}
<div className="border-t border-light-200 dark:border-dark-200 pt-2 mt-1">
<label className="flex items-center space-x-2 p-2 rounded-lg cursor-pointer hover:bg-light-secondary dark:hover:bg-dark-secondary">
<input
type="checkbox"
checked={isCompact}
onChange={(e) => handleCompactChange(e.target.checked)}
className="form-checkbox h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
/>
<div className="flex items-center space-x-2">
<Minimize2
size={16}
className="text-gray-600 dark:text-gray-400"
/>
<div>
<p className="text-sm font-medium text-black dark:text-white">
Compact Mode
</p>
<p className="text-xs text-black/70 dark:text-white/70">
Generate more concise responses
</p>
</div>
</div>
</label>
</div>
</div>
</PopoverPanel>
</Transition>