mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-09-16 22:31:32 +00:00
Compare commits
1 Commits
5a23406dd2
...
3ed7f6ad17
Author | SHA1 | Date | |
---|---|---|---|
|
3ed7f6ad17 |
@@ -19,8 +19,6 @@ const Chat = ({
|
|||||||
setFiles,
|
setFiles,
|
||||||
optimizationMode,
|
optimizationMode,
|
||||||
setOptimizationMode,
|
setOptimizationMode,
|
||||||
focusMode,
|
|
||||||
setFocusMode,
|
|
||||||
}: {
|
}: {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
sendMessage: (
|
sendMessage: (
|
||||||
@@ -40,15 +38,13 @@ const Chat = ({
|
|||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
optimizationMode: string;
|
optimizationMode: string;
|
||||||
setOptimizationMode: (mode: string) => void;
|
setOptimizationMode: (mode: string) => void;
|
||||||
focusMode: string;
|
|
||||||
setFocusMode: (mode: string) => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [dividerWidth, setDividerWidth] = useState(0);
|
const [dividerWidth, setDividerWidth] = useState(0);
|
||||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||||
const [manuallyScrolledUp, setManuallyScrolledUp] = useState(false);
|
const [manuallyScrolledUp, setManuallyScrolledUp] = useState(false);
|
||||||
const dividerRef = useRef<HTMLDivElement | null>(null);
|
const dividerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const messageEnd = useRef<HTMLDivElement | null>(null);
|
const messageEnd = useRef<HTMLDivElement | null>(null);
|
||||||
const SCROLL_THRESHOLD = 250; // pixels from bottom to consider "at bottom"
|
const SCROLL_THRESHOLD = 200; // pixels from bottom to consider "at bottom"
|
||||||
|
|
||||||
// Check if user is at bottom of page
|
// Check if user is at bottom of page
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -150,6 +146,7 @@ const Chat = ({
|
|||||||
const position = window.innerHeight + window.scrollY;
|
const position = window.innerHeight + window.scrollY;
|
||||||
const height = document.body.scrollHeight;
|
const height = document.body.scrollHeight;
|
||||||
const atBottom = position >= height - SCROLL_THRESHOLD;
|
const atBottom = position >= height - SCROLL_THRESHOLD;
|
||||||
|
console.log('scrollTrigger', scrollTrigger);
|
||||||
setIsAtBottom(atBottom);
|
setIsAtBottom(atBottom);
|
||||||
|
|
||||||
if (isAtBottom && !manuallyScrolledUp && messages.length > 0) {
|
if (isAtBottom && !manuallyScrolledUp && messages.length > 0) {
|
||||||
@@ -158,7 +155,7 @@ const Chat = ({
|
|||||||
}, [scrollTrigger, isAtBottom, messages.length, manuallyScrolledUp]);
|
}, [scrollTrigger, isAtBottom, messages.length, manuallyScrolledUp]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-6 pt-8 pb-48 sm:mx-4 md:mx-8">
|
<div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-32 sm:mx-4 md:mx-8">
|
||||||
{messages.map((msg, i) => {
|
{messages.map((msg, i) => {
|
||||||
const isLast = i === messages.length - 1;
|
const isLast = i === messages.length - 1;
|
||||||
|
|
||||||
@@ -220,7 +217,6 @@ const Chat = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<MessageInput
|
<MessageInput
|
||||||
firstMessage={messages.length === 0}
|
|
||||||
loading={loading}
|
loading={loading}
|
||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
fileIds={fileIds}
|
fileIds={fileIds}
|
||||||
@@ -229,8 +225,6 @@ const Chat = ({
|
|||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
optimizationMode={optimizationMode}
|
optimizationMode={optimizationMode}
|
||||||
setOptimizationMode={setOptimizationMode}
|
setOptimizationMode={setOptimizationMode}
|
||||||
focusMode={focusMode}
|
|
||||||
setFocusMode={setFocusMode}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@@ -531,15 +531,6 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
const ollamaContextWindow =
|
const ollamaContextWindow =
|
||||||
localStorage.getItem('ollamaContextWindow') || '2048';
|
localStorage.getItem('ollamaContextWindow') || '2048';
|
||||||
|
|
||||||
// Get the latest model selection from localStorage
|
|
||||||
const currentChatModelProvider = localStorage.getItem('chatModelProvider');
|
|
||||||
const currentChatModel = localStorage.getItem('chatModel');
|
|
||||||
|
|
||||||
// Use the most current model selection from localStorage, falling back to the state if not available
|
|
||||||
const modelProvider =
|
|
||||||
currentChatModelProvider || chatModelProvider.provider;
|
|
||||||
const modelName = currentChatModel || chatModelProvider.name;
|
|
||||||
|
|
||||||
const res = await fetch('/api/chat', {
|
const res = await fetch('/api/chat', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -558,8 +549,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
optimizationMode: optimizationMode,
|
optimizationMode: optimizationMode,
|
||||||
history: messageChatHistory,
|
history: messageChatHistory,
|
||||||
chatModel: {
|
chatModel: {
|
||||||
name: modelName,
|
name: chatModelProvider.name,
|
||||||
provider: modelProvider,
|
provider: chatModelProvider.provider,
|
||||||
...(chatModelProvider.provider === 'ollama' && {
|
...(chatModelProvider.provider === 'ollama' && {
|
||||||
ollamaContextWindow: parseInt(ollamaContextWindow),
|
ollamaContextWindow: parseInt(ollamaContextWindow),
|
||||||
}),
|
}),
|
||||||
@@ -654,8 +645,6 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
optimizationMode={optimizationMode}
|
optimizationMode={optimizationMode}
|
||||||
setOptimizationMode={setOptimizationMode}
|
setOptimizationMode={setOptimizationMode}
|
||||||
focusMode={focusMode}
|
|
||||||
setFocusMode={setFocusMode}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { Settings } from 'lucide-react';
|
import { Settings } from 'lucide-react';
|
||||||
|
import EmptyChatMessageInput from './EmptyChatMessageInput';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { File } from './ChatWindow';
|
import { File } from './ChatWindow';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import MessageInput from './MessageInput';
|
|
||||||
|
|
||||||
const EmptyChat = ({
|
const EmptyChat = ({
|
||||||
sendMessage,
|
sendMessage,
|
||||||
@@ -38,9 +38,7 @@ const EmptyChat = ({
|
|||||||
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
|
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
|
||||||
Research begins here.
|
Research begins here.
|
||||||
</h2>
|
</h2>
|
||||||
<MessageInput
|
<EmptyChatMessageInput
|
||||||
firstMessage={true}
|
|
||||||
loading={false}
|
|
||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
focusMode={focusMode}
|
focusMode={focusMode}
|
||||||
setFocusMode={setFocusMode}
|
setFocusMode={setFocusMode}
|
||||||
|
114
src/components/EmptyChatMessageInput.tsx
Normal file
114
src/components/EmptyChatMessageInput.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
|
import CopilotToggle from './MessageInputActions/Copilot';
|
||||||
|
import Focus from './MessageInputActions/Focus';
|
||||||
|
import Optimization from './MessageInputActions/Optimization';
|
||||||
|
import Attach from './MessageInputActions/Attach';
|
||||||
|
import { File } from './ChatWindow';
|
||||||
|
|
||||||
|
const EmptyChatMessageInput = ({
|
||||||
|
sendMessage,
|
||||||
|
focusMode,
|
||||||
|
setFocusMode,
|
||||||
|
optimizationMode,
|
||||||
|
setOptimizationMode,
|
||||||
|
fileIds,
|
||||||
|
setFileIds,
|
||||||
|
files,
|
||||||
|
setFiles,
|
||||||
|
}: {
|
||||||
|
sendMessage: (message: string) => void;
|
||||||
|
focusMode: string;
|
||||||
|
setFocusMode: (mode: string) => void;
|
||||||
|
optimizationMode: string;
|
||||||
|
setOptimizationMode: (mode: string) => void;
|
||||||
|
fileIds: string[];
|
||||||
|
setFileIds: (fileIds: string[]) => void;
|
||||||
|
files: File[];
|
||||||
|
setFiles: (files: File[]) => void;
|
||||||
|
}) => {
|
||||||
|
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
inputRef.current?.focus();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage(message);
|
||||||
|
setMessage('');
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage(message);
|
||||||
|
setMessage('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col bg-light-secondary dark:bg-dark-secondary px-5 pt-5 pb-2 rounded-lg w-full border border-light-200 dark:border-dark-200">
|
||||||
|
<TextareaAutosize
|
||||||
|
ref={inputRef}
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
minRows={2}
|
||||||
|
className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
|
||||||
|
placeholder="Ask anything..."
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row items-center justify-between mt-4">
|
||||||
|
<div className="flex flex-row items-center space-x-2 lg:space-x-4">
|
||||||
|
<Focus focusMode={focusMode} setFocusMode={setFocusMode} />
|
||||||
|
<Attach
|
||||||
|
fileIds={fileIds}
|
||||||
|
setFileIds={setFileIds}
|
||||||
|
files={files}
|
||||||
|
setFiles={setFiles}
|
||||||
|
showText
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center space-x-1 sm:space-x-4">
|
||||||
|
<Optimization
|
||||||
|
optimizationMode={optimizationMode}
|
||||||
|
setOptimizationMode={setOptimizationMode}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
disabled={message.trim().length === 0}
|
||||||
|
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2"
|
||||||
|
>
|
||||||
|
<ArrowRight className="bg-background" size={17} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmptyChatMessageInput;
|
@@ -1,11 +1,12 @@
|
|||||||
import { ArrowRight, ArrowUp } from 'lucide-react';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { ArrowUp } from 'lucide-react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { File } from './ChatWindow';
|
|
||||||
import Attach from './MessageInputActions/Attach';
|
import Attach from './MessageInputActions/Attach';
|
||||||
import Focus from './MessageInputActions/Focus';
|
import CopilotToggle from './MessageInputActions/Copilot';
|
||||||
import ModelSelector from './MessageInputActions/ModelSelector';
|
|
||||||
import Optimization from './MessageInputActions/Optimization';
|
import Optimization from './MessageInputActions/Optimization';
|
||||||
|
import { File } from './ChatWindow';
|
||||||
|
import AttachSmall from './MessageInputActions/AttachSmall';
|
||||||
|
|
||||||
const MessageInput = ({
|
const MessageInput = ({
|
||||||
sendMessage,
|
sendMessage,
|
||||||
@@ -16,9 +17,6 @@ const MessageInput = ({
|
|||||||
setFiles,
|
setFiles,
|
||||||
optimizationMode,
|
optimizationMode,
|
||||||
setOptimizationMode,
|
setOptimizationMode,
|
||||||
focusMode,
|
|
||||||
setFocusMode,
|
|
||||||
firstMessage,
|
|
||||||
}: {
|
}: {
|
||||||
sendMessage: (message: string) => void;
|
sendMessage: (message: string) => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -28,28 +26,19 @@ const MessageInput = ({
|
|||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
optimizationMode: string;
|
optimizationMode: string;
|
||||||
setOptimizationMode: (mode: string) => void;
|
setOptimizationMode: (mode: string) => void;
|
||||||
focusMode: string;
|
|
||||||
setFocusMode: (mode: string) => void;
|
|
||||||
firstMessage: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [selectedModel, setSelectedModel] = useState<{
|
const [textareaRows, setTextareaRows] = useState(1);
|
||||||
provider: string;
|
const [mode, setMode] = useState<'multi' | 'single'>('single');
|
||||||
model: string;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load saved model preferences from localStorage
|
if (textareaRows >= 2 && message && mode === 'single') {
|
||||||
const chatModelProvider = localStorage.getItem('chatModelProvider');
|
setMode('multi');
|
||||||
const chatModel = localStorage.getItem('chatModel');
|
} else if (!message && mode === 'multi') {
|
||||||
|
setMode('single');
|
||||||
if (chatModelProvider && chatModel) {
|
|
||||||
setSelectedModel({
|
|
||||||
provider: chatModelProvider,
|
|
||||||
model: chatModel,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, [textareaRows, mode, message]);
|
||||||
|
|
||||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
|
||||||
@@ -71,74 +60,117 @@ const MessageInput = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Function to handle message submission
|
return (
|
||||||
const handleSubmitMessage = () => {
|
|
||||||
// Only submit if we have a non-empty message and not currently loading
|
|
||||||
if (loading || message.trim().length === 0) return;
|
|
||||||
|
|
||||||
// Make sure the selected model is used when sending a message
|
|
||||||
if (selectedModel) {
|
|
||||||
localStorage.setItem('chatModelProvider', selectedModel.provider);
|
|
||||||
localStorage.setItem('chatModel', selectedModel.model);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMessage(message);
|
|
||||||
setMessage('');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
|
if (loading) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSubmitMessage();
|
sendMessage(message);
|
||||||
|
setMessage('');
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey && !loading) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSubmitMessage();
|
sendMessage(message);
|
||||||
|
setMessage('');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
className={cn(
|
||||||
|
'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-col md:flex-row rounded-lg md:rounded-full',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col bg-light-secondary dark:bg-dark-secondary px-5 pt-5 pb-2 rounded-lg w-full border border-light-200 dark:border-dark-200">
|
{mode === 'single' && (
|
||||||
<TextareaAutosize
|
<div className="flex flex-row items-center justify-between w-full mb-2 md:mb-0 md:w-auto">
|
||||||
ref={inputRef}
|
<div className="flex flex-row items-center space-x-2">
|
||||||
value={message}
|
<AttachSmall
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
minRows={2}
|
|
||||||
className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
|
|
||||||
placeholder={firstMessage ? "Ask anything..." :"Ask a follow-up"}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-row items-center justify-between mt-4">
|
|
||||||
<div className="flex flex-row items-center space-x-2 lg:space-x-4">
|
|
||||||
<Focus focusMode={focusMode} setFocusMode={setFocusMode} />
|
|
||||||
<Attach
|
|
||||||
fileIds={fileIds}
|
fileIds={fileIds}
|
||||||
setFileIds={setFileIds}
|
setFileIds={setFileIds}
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
showText={firstMessage}
|
|
||||||
/>
|
/>
|
||||||
<ModelSelector
|
|
||||||
selectedModel={selectedModel}
|
|
||||||
setSelectedModel={setSelectedModel}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center space-x-1 sm:space-x-4">
|
|
||||||
<Optimization
|
<Optimization
|
||||||
optimizationMode={optimizationMode}
|
optimizationMode={optimizationMode}
|
||||||
setOptimizationMode={setOptimizationMode}
|
setOptimizationMode={setOptimizationMode}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="md:hidden">
|
||||||
|
<CopilotToggle
|
||||||
|
copilotEnabled={copilotEnabled}
|
||||||
|
setCopilotEnabled={setCopilotEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-row items-center w-full">
|
||||||
|
<TextareaAutosize
|
||||||
|
ref={inputRef}
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
onHeightChange={(height, props) => {
|
||||||
|
setTextareaRows(Math.ceil(height / props.rowHeight));
|
||||||
|
}}
|
||||||
|
className="transition bg-transparent dark:placeholder:text-white/50 placeholder:text-sm text-sm dark:text-white resize-none focus:outline-none w-full px-2 max-h-24 lg:max-h-36 xl:max-h-48 flex-grow flex-shrink"
|
||||||
|
placeholder="Ask a follow-up"
|
||||||
|
/>
|
||||||
|
{mode === 'single' && (
|
||||||
|
<div className="flex flex-row items-center space-x-4">
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<CopilotToggle
|
||||||
|
copilotEnabled={copilotEnabled}
|
||||||
|
setCopilotEnabled={setCopilotEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
disabled={message.trim().length === 0}
|
disabled={message.trim().length === 0 || loading}
|
||||||
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2"
|
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
|
||||||
type="submit"
|
|
||||||
>
|
>
|
||||||
{firstMessage ? <ArrowRight className="bg-background" size={17} /> : <ArrowUp className="bg-background" size={17} />}
|
<ArrowUp className="bg-background" size={17} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mode === 'multi' && (
|
||||||
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between w-full pt-2">
|
||||||
|
<div className="flex flex-row items-center justify-between w-full md:w-auto mb-2 md:mb-0">
|
||||||
|
<div className="flex flex-row items-center space-x-2">
|
||||||
|
<AttachSmall
|
||||||
|
fileIds={fileIds}
|
||||||
|
setFileIds={setFileIds}
|
||||||
|
files={files}
|
||||||
|
setFiles={setFiles}
|
||||||
|
/>
|
||||||
|
<Optimization
|
||||||
|
optimizationMode={optimizationMode}
|
||||||
|
setOptimizationMode={setOptimizationMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="md:hidden">
|
||||||
|
<CopilotToggle
|
||||||
|
copilotEnabled={copilotEnabled}
|
||||||
|
setCopilotEnabled={setCopilotEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center space-x-4 self-end">
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<CopilotToggle
|
||||||
|
copilotEnabled={copilotEnabled}
|
||||||
|
setCopilotEnabled={setCopilotEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
disabled={message.trim().length === 0 || loading}
|
||||||
|
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
|
||||||
|
>
|
||||||
|
<ArrowUp className="bg-background" size={17} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -5,7 +5,7 @@ import {
|
|||||||
PopoverPanel,
|
PopoverPanel,
|
||||||
Transition,
|
Transition,
|
||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import { File, LoaderCircle, Paperclip, Plus, Trash } from 'lucide-react';
|
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
|
||||||
import { Fragment, useRef, useState } from 'react';
|
import { Fragment, useRef, useState } from 'react';
|
||||||
import { File as FileType } from '../ChatWindow';
|
import { File as FileType } from '../ChatWindow';
|
||||||
|
|
||||||
@@ -176,10 +176,8 @@ const Attach = ({
|
|||||||
multiple
|
multiple
|
||||||
hidden
|
hidden
|
||||||
/>
|
/>
|
||||||
<Paperclip size="18" />
|
<CopyPlus size={showText ? 18 : undefined} />
|
||||||
{showText && (
|
{showText && <p className="text-xs font-medium pl-[1px]">Attach</p>}
|
||||||
<p className="text-xs font-medium pl-[1px] hidden lg:block">Attach</p>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -93,13 +93,13 @@ const Focus = ({
|
|||||||
<Transition
|
<Transition
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="transition ease-out duration-150"
|
enter="transition ease-out duration-150"
|
||||||
enterFrom="opacity-0 -translate-y-1"
|
enterFrom="opacity-0 translate-y-1"
|
||||||
enterTo="opacity-100 translate-y-0"
|
enterTo="opacity-100 translate-y-0"
|
||||||
leave="transition ease-in duration-150"
|
leave="transition ease-in duration-150"
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 -translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<PopoverPanel className="absolute z-10 w-64 md:w-[500px] left-0 bottom-full mb-2">
|
<PopoverPanel className="absolute z-10 w-64 md:w-[500px] left-0">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 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">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 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">
|
||||||
{focusModes.map((mode, i) => (
|
{focusModes.map((mode, i) => (
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
|
@@ -1,305 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Cpu, ChevronDown, ChevronRight } from 'lucide-react';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverButton,
|
|
||||||
PopoverPanel,
|
|
||||||
Transition,
|
|
||||||
} from '@headlessui/react';
|
|
||||||
import { Fragment } from 'react';
|
|
||||||
|
|
||||||
interface ModelOption {
|
|
||||||
provider: string;
|
|
||||||
model: string;
|
|
||||||
displayName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProviderModelMap {
|
|
||||||
[provider: string]: {
|
|
||||||
displayName: string;
|
|
||||||
models: ModelOption[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModelSelector = ({
|
|
||||||
selectedModel,
|
|
||||||
setSelectedModel,
|
|
||||||
}: {
|
|
||||||
selectedModel: { provider: string; model: string } | null;
|
|
||||||
setSelectedModel: (model: { provider: string; model: string }) => void;
|
|
||||||
}) => {
|
|
||||||
const [providerModels, setProviderModels] = useState<ProviderModelMap>({});
|
|
||||||
const [providersList, setProvidersList] = useState<string[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [selectedModelDisplay, setSelectedModelDisplay] = useState<string>('');
|
|
||||||
const [selectedProviderDisplay, setSelectedProviderDisplay] =
|
|
||||||
useState<string>('');
|
|
||||||
const [expandedProviders, setExpandedProviders] = useState<
|
|
||||||
Record<string, boolean>
|
|
||||||
>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchModels = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/models', {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch models: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const providersData: ProviderModelMap = {};
|
|
||||||
|
|
||||||
// Organize models by provider
|
|
||||||
Object.entries(data.chatModelProviders).forEach(
|
|
||||||
([provider, models]: [string, any]) => {
|
|
||||||
const providerDisplayName =
|
|
||||||
provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
||||||
providersData[provider] = {
|
|
||||||
displayName: providerDisplayName,
|
|
||||||
models: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.entries(models).forEach(
|
|
||||||
([modelKey, modelData]: [string, any]) => {
|
|
||||||
providersData[provider].models.push({
|
|
||||||
provider,
|
|
||||||
model: modelKey,
|
|
||||||
displayName: modelData.displayName || modelKey,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Filter out providers with no models
|
|
||||||
Object.keys(providersData).forEach((provider) => {
|
|
||||||
if (providersData[provider].models.length === 0) {
|
|
||||||
delete providersData[provider];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort providers by name (only those that have models)
|
|
||||||
const sortedProviders = Object.keys(providersData).sort();
|
|
||||||
setProvidersList(sortedProviders);
|
|
||||||
|
|
||||||
// Initialize expanded state for all providers
|
|
||||||
const initialExpandedState: Record<string, boolean> = {};
|
|
||||||
sortedProviders.forEach((provider) => {
|
|
||||||
initialExpandedState[provider] = selectedModel?.provider === provider;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expand the first provider if none is selected
|
|
||||||
if (sortedProviders.length > 0 && !selectedModel) {
|
|
||||||
initialExpandedState[sortedProviders[0]] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
setExpandedProviders(initialExpandedState);
|
|
||||||
setProviderModels(providersData);
|
|
||||||
|
|
||||||
// Find the current model in our options to display its name
|
|
||||||
if (selectedModel) {
|
|
||||||
const provider = providersData[selectedModel.provider];
|
|
||||||
if (provider) {
|
|
||||||
const currentModel = provider.models.find(
|
|
||||||
(option) => option.model === selectedModel.model,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentModel) {
|
|
||||||
setSelectedModelDisplay(currentModel.displayName);
|
|
||||||
setSelectedProviderDisplay(provider.displayName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching models:', error);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchModels();
|
|
||||||
}, [selectedModel, setSelectedModel]);
|
|
||||||
|
|
||||||
const toggleProviderExpanded = (provider: string) => {
|
|
||||||
setExpandedProviders((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[provider]: !prev[provider],
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectModel = (option: ModelOption) => {
|
|
||||||
setSelectedModel({
|
|
||||||
provider: option.provider,
|
|
||||||
model: option.model,
|
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedModelDisplay(option.displayName);
|
|
||||||
setSelectedProviderDisplay(
|
|
||||||
providerModels[option.provider]?.displayName || option.provider,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save to localStorage for persistence
|
|
||||||
localStorage.setItem('chatModelProvider', option.provider);
|
|
||||||
localStorage.setItem('chatModel', option.model);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDisplayText = () => {
|
|
||||||
if (loading) return 'Loading...';
|
|
||||||
if (!selectedModelDisplay) return 'Select model';
|
|
||||||
|
|
||||||
return `${selectedModelDisplay} (${selectedProviderDisplay})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover className="relative">
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<div className="relative">
|
|
||||||
<PopoverButton className="group flex items-center justify-center 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">
|
|
||||||
<Cpu size={18} />
|
|
||||||
<span className="mx-2 text-xs font-medium overflow-hidden text-ellipsis whitespace-nowrap max-w-44 hidden lg:block">
|
|
||||||
{getDisplayText()}
|
|
||||||
</span>
|
|
||||||
<ChevronDown
|
|
||||||
size={16}
|
|
||||||
className={cn(
|
|
||||||
'transition-transform',
|
|
||||||
open ? 'rotate-180' : 'rotate-0',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</PopoverButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0 translate-y-1"
|
|
||||||
enterTo="opacity-100 translate-y-0"
|
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<PopoverPanel className="absolute z-10 w-72 transform bottom-full mb-2">
|
|
||||||
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black/5 dark:ring-white/5 bg-white dark:bg-dark-secondary divide-y divide-light-200 dark:divide-dark-200">
|
|
||||||
<div className="px-4 py-3">
|
|
||||||
<h3 className="text-sm font-medium text-black/90 dark:text-white/90">
|
|
||||||
Select Model
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs text-black/60 dark:text-white/60 mt-1">
|
|
||||||
Choose a provider and model for your conversation
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-72 overflow-y-auto">
|
|
||||||
{loading ? (
|
|
||||||
<div className="px-4 py-3 text-sm text-black/70 dark:text-white/70">
|
|
||||||
Loading available models...
|
|
||||||
</div>
|
|
||||||
) : providersList.length === 0 ? (
|
|
||||||
<div className="px-4 py-3 text-sm text-black/70 dark:text-white/70">
|
|
||||||
No models available
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="py-1">
|
|
||||||
{providersList.map((providerKey) => {
|
|
||||||
const provider = providerModels[providerKey];
|
|
||||||
const isExpanded = expandedProviders[providerKey];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={providerKey}
|
|
||||||
className="border-t border-light-200 dark:border-dark-200 first:border-t-0"
|
|
||||||
>
|
|
||||||
{/* Provider header */}
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
'w-full flex items-center justify-between px-4 py-2 text-sm text-left',
|
|
||||||
'hover:bg-light-100 dark:hover:bg-dark-100',
|
|
||||||
selectedModel?.provider === providerKey
|
|
||||||
? 'bg-light-50 dark:bg-dark-50'
|
|
||||||
: '',
|
|
||||||
)}
|
|
||||||
onClick={() =>
|
|
||||||
toggleProviderExpanded(providerKey)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="font-medium flex items-center">
|
|
||||||
<Cpu
|
|
||||||
size={14}
|
|
||||||
className="mr-2 text-black/70 dark:text-white/70"
|
|
||||||
/>
|
|
||||||
{provider.displayName}
|
|
||||||
{selectedModel?.provider === providerKey && (
|
|
||||||
<span className="ml-2 text-xs text-[#24A0ED]">
|
|
||||||
(active)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<ChevronRight
|
|
||||||
size={14}
|
|
||||||
className={cn(
|
|
||||||
'transition-transform',
|
|
||||||
isExpanded ? 'rotate-90' : '',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Models list */}
|
|
||||||
{isExpanded && (
|
|
||||||
<div className="pl-6">
|
|
||||||
{provider.models.map((modelOption) => (
|
|
||||||
<button
|
|
||||||
key={`${modelOption.provider}-${modelOption.model}`}
|
|
||||||
className={cn(
|
|
||||||
'w-full text-left px-4 py-2 text-sm flex items-center',
|
|
||||||
selectedModel?.provider ===
|
|
||||||
modelOption.provider &&
|
|
||||||
selectedModel?.model ===
|
|
||||||
modelOption.model
|
|
||||||
? 'bg-light-100 dark:bg-dark-100 text-black dark:text-white'
|
|
||||||
: 'text-black/70 dark:text-white/70 hover:bg-light-100 dark:hover:bg-dark-100',
|
|
||||||
)}
|
|
||||||
onClick={() =>
|
|
||||||
handleSelectModel(modelOption)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col flex-1">
|
|
||||||
<span className="font-medium">
|
|
||||||
{modelOption.displayName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/* Active indicator */}
|
|
||||||
{selectedModel?.provider ===
|
|
||||||
modelOption.provider &&
|
|
||||||
selectedModel?.model ===
|
|
||||||
modelOption.model && (
|
|
||||||
<div className="ml-auto bg-[#24A0ED] text-white text-xs px-1.5 py-0.5 rounded">
|
|
||||||
Active
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverPanel>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModelSelector;
|
|
@@ -56,12 +56,12 @@ const Optimization = ({
|
|||||||
OptimizationModes.find((mode) => mode.key === optimizationMode)
|
OptimizationModes.find((mode) => mode.key === optimizationMode)
|
||||||
?.icon
|
?.icon
|
||||||
}
|
}
|
||||||
{/* <p className="text-xs font-medium hidden lg:block">
|
<p className="text-xs font-medium">
|
||||||
{
|
{
|
||||||
OptimizationModes.find((mode) => mode.key === optimizationMode)
|
OptimizationModes.find((mode) => mode.key === optimizationMode)
|
||||||
?.title
|
?.title
|
||||||
}
|
}
|
||||||
</p> */}
|
</p>
|
||||||
<ChevronDown size={20} />
|
<ChevronDown size={20} />
|
||||||
</div>
|
</div>
|
||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
|
@@ -96,14 +96,7 @@ export const getAvailableChatModelProviders = async () => {
|
|||||||
for (const provider in chatModelProviders) {
|
for (const provider in chatModelProviders) {
|
||||||
const providerModels = await chatModelProviders[provider]();
|
const providerModels = await chatModelProviders[provider]();
|
||||||
if (Object.keys(providerModels).length > 0) {
|
if (Object.keys(providerModels).length > 0) {
|
||||||
// Sort models alphabetically by their keys
|
models[provider] = providerModels;
|
||||||
const sortedModels: Record<string, ChatModel> = {};
|
|
||||||
Object.keys(providerModels)
|
|
||||||
.sort()
|
|
||||||
.forEach((key) => {
|
|
||||||
sortedModels[key] = providerModels[key];
|
|
||||||
});
|
|
||||||
models[provider] = sortedModels;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,14 +131,7 @@ export const getAvailableEmbeddingModelProviders = async () => {
|
|||||||
for (const provider in embeddingModelProviders) {
|
for (const provider in embeddingModelProviders) {
|
||||||
const providerModels = await embeddingModelProviders[provider]();
|
const providerModels = await embeddingModelProviders[provider]();
|
||||||
if (Object.keys(providerModels).length > 0) {
|
if (Object.keys(providerModels).length > 0) {
|
||||||
// Sort embedding models alphabetically by their keys
|
models[provider] = providerModels;
|
||||||
const sortedModels: Record<string, EmbeddingModel> = {};
|
|
||||||
Object.keys(providerModels)
|
|
||||||
.sort()
|
|
||||||
.forEach((key) => {
|
|
||||||
sortedModels[key] = providerModels[key];
|
|
||||||
});
|
|
||||||
models[provider] = sortedModels;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user