This commit is contained in:
Willie Zutz
2025-03-02 01:16:09 +08:00
committed by GitHub
21 changed files with 410 additions and 68 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

@ -197,6 +197,11 @@ const useSocket = (
'openAIBaseURL',
localStorage.getItem('openAIBaseURL')!,
);
} else {
searchParams.append(
'ollamaContextWindow',
localStorage.getItem('ollamaContextWindow') || '2048',
);
}
searchParams.append('embeddingModel', embeddingModel!);
@ -394,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);
@ -401,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 &&
@ -451,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; rewriteIndex?: number },
) => {
if (loading) return;
if (!ws || ws.readyState !== WebSocket.OPEN) {
toast.error('Cannot send message while disconnected');
@ -464,23 +489,33 @@ const ChatWindow = ({ id }: { id?: string }) => {
let sources: Document[] | undefined = undefined;
let recievedMessage = '';
let added = false;
let messageChatHistory = chatHistory;
if (options?.rewriteIndex !== undefined) {
const rewriteIndex = options.rewriteIndex;
setMessages((prev) => {
return [...prev.slice(0, messages.length > 2 ? rewriteIndex - 1 : 0)]
});
messageChatHistory = chatHistory.slice(0, messages.length > 2 ? rewriteIndex - 1 : 0)
setChatHistory(messageChatHistory);
}
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: [...messageChatHistory, ['human', message]],
isCompact: options?.isCompact ?? isCompact,
};
ws.send(JSON.stringify(messageData));
setMessages((prevMessages) => [
...prevMessages,
@ -597,25 +632,14 @@ const ChatWindow = ({ id }: { id?: string }) => {
};
const rewrite = (messageId: string) => {
const index = messages.findIndex((msg) => msg.messageId === messageId);
if (index === -1) return;
const message = messages[index - 1];
setMessages((prev) => {
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
});
setChatHistory((prev) => {
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
});
sendMessage(message.content, message.messageId);
const messageIndex = messages.findIndex((msg) => msg.messageId === messageId);
if(messageIndex == -1) return;
sendMessage(messages[messageIndex - 1].content, messageId, { isCompact, rewriteIndex: messageIndex });
};
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]);
@ -655,6 +679,10 @@ const ChatWindow = ({ id }: { id?: string }) => {
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
isCompact={isCompact}
setIsCompact={setIsCompact}
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
</>
) : (
@ -668,6 +696,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
@ -48,6 +70,12 @@ const Optimization = ({
className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
>
<div className="flex flex-row items-center space-x-1">
{isCompact && (
<Minimize2
size={16}
className="text-gray-600 dark:text-gray-400"
/>
)}
{
OptimizationModes.find((mode) => mode.key === optimizationMode)
?.icon
@ -70,11 +98,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 +122,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>

View File

@ -33,9 +33,10 @@ const SearchImages = ({
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const ollamaContextWindow =
localStorage.getItem('ollamaContextWindow') || '2048';
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/images`,
@ -54,6 +55,9 @@ const SearchImages = ({
customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey,
}),
...(chatModelProvider === 'ollama' && {
ollamaContextWindow: parseInt(ollamaContextWindow),
}),
},
}),
},

View File

@ -48,9 +48,10 @@ const Searchvideos = ({
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const ollamaContextWindow =
localStorage.getItem('ollamaContextWindow') || '2048';
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/videos`,
@ -69,6 +70,9 @@ const Searchvideos = ({
customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey,
}),
...(chatModelProvider === 'ollama' && {
ollamaContextWindow: parseInt(ollamaContextWindow),
}),
},
}),
},