prettier
This commit is contained in:
parent
5b1aaee605
commit
3b737a078a
63 changed files with 1132 additions and 1853 deletions
|
@ -1,10 +1,10 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import MessageInput from './MessageInput';
|
||||
import { Message } from './ChatWindow';
|
||||
import MessageBox from './MessageBox';
|
||||
import MessageBoxLoading from './MessageBoxLoading';
|
||||
import { Fragment, useEffect, useRef, useState } from "react";
|
||||
import MessageInput from "./MessageInput";
|
||||
import { Message } from "./ChatWindow";
|
||||
import MessageBox from "./MessageBox";
|
||||
import MessageBoxLoading from "./MessageBoxLoading";
|
||||
|
||||
const Chat = ({
|
||||
loading,
|
||||
|
@ -32,15 +32,15 @@ const Chat = ({
|
|||
|
||||
updateDividerWidth();
|
||||
|
||||
window.addEventListener('resize', updateDividerWidth);
|
||||
window.addEventListener("resize", updateDividerWidth);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateDividerWidth);
|
||||
window.removeEventListener("resize", updateDividerWidth);
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
messageEnd.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
messageEnd.current?.scrollIntoView({ behavior: "smooth" });
|
||||
|
||||
if (messages.length === 1) {
|
||||
document.title = `${messages[0].content.substring(0, 30)} - Perplexica`;
|
||||
|
@ -65,7 +65,7 @@ const Chat = ({
|
|||
rewrite={rewrite}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
{!isLast && msg.role === 'assistant' && (
|
||||
{!isLast && msg.role === "assistant" && (
|
||||
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||
)}
|
||||
</Fragment>
|
||||
|
@ -74,10 +74,7 @@ const Chat = ({
|
|||
{loading && !messageAppeared && <MessageBoxLoading />}
|
||||
<div ref={messageEnd} className="h-0" />
|
||||
{dividerWidth > 0 && (
|
||||
<div
|
||||
className="bottom-24 lg:bottom-10 fixed z-40"
|
||||
style={{ width: dividerWidth }}
|
||||
>
|
||||
<div className="bottom-24 lg:bottom-10 fixed z-40" style={{ width: dividerWidth }}>
|
||||
<MessageInput loading={loading} sendMessage={sendMessage} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,110 +1,79 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Document } from '@langchain/core/documents';
|
||||
import Navbar from './Navbar';
|
||||
import Chat from './Chat';
|
||||
import EmptyChat from './EmptyChat';
|
||||
import crypto from 'crypto';
|
||||
import { toast } from 'sonner';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { getSuggestions } from '@/lib/actions';
|
||||
import Error from 'next/error';
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Document } from "@langchain/core/documents";
|
||||
import Navbar from "./Navbar";
|
||||
import Chat from "./Chat";
|
||||
import EmptyChat from "./EmptyChat";
|
||||
import crypto from "crypto";
|
||||
import { toast } from "sonner";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { getSuggestions } from "@/lib/actions";
|
||||
import Error from "next/error";
|
||||
|
||||
export type Message = {
|
||||
messageId: string;
|
||||
chatId: string;
|
||||
createdAt: Date;
|
||||
content: string;
|
||||
role: 'user' | 'assistant';
|
||||
role: "user" | "assistant";
|
||||
suggestions?: string[];
|
||||
sources?: Document[];
|
||||
};
|
||||
|
||||
const useSocket = (
|
||||
url: string,
|
||||
setIsWSReady: (ready: boolean) => void,
|
||||
setError: (error: boolean) => void,
|
||||
) => {
|
||||
const useSocket = (url: string, setIsWSReady: (ready: boolean) => void, setError: (error: boolean) => void) => {
|
||||
const [ws, setWs] = useState<WebSocket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ws) {
|
||||
const connectWs = async () => {
|
||||
let chatModel = localStorage.getItem('chatModel');
|
||||
let chatModelProvider = localStorage.getItem('chatModelProvider');
|
||||
let embeddingModel = localStorage.getItem('embeddingModel');
|
||||
let embeddingModelProvider = localStorage.getItem(
|
||||
'embeddingModelProvider',
|
||||
);
|
||||
let chatModel = localStorage.getItem("chatModel");
|
||||
let chatModelProvider = localStorage.getItem("chatModelProvider");
|
||||
let embeddingModel = localStorage.getItem("embeddingModel");
|
||||
let embeddingModelProvider = localStorage.getItem("embeddingModelProvider");
|
||||
|
||||
if (
|
||||
!chatModel ||
|
||||
!chatModelProvider ||
|
||||
!embeddingModel ||
|
||||
!embeddingModelProvider
|
||||
) {
|
||||
const providers = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/models`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
if (!chatModel || !chatModelProvider || !embeddingModel || !embeddingModelProvider) {
|
||||
const providers = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/models`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
).then(async (res) => await res.json());
|
||||
}).then(async res => await res.json());
|
||||
|
||||
const chatModelProviders = providers.chatModelProviders;
|
||||
const embeddingModelProviders = providers.embeddingModelProviders;
|
||||
|
||||
if (
|
||||
!chatModelProviders ||
|
||||
Object.keys(chatModelProviders).length === 0
|
||||
)
|
||||
return toast.error('No chat models available');
|
||||
if (!chatModelProviders || Object.keys(chatModelProviders).length === 0)
|
||||
return toast.error("No chat models available");
|
||||
|
||||
if (
|
||||
!embeddingModelProviders ||
|
||||
Object.keys(embeddingModelProviders).length === 0
|
||||
)
|
||||
return toast.error('No embedding models available');
|
||||
if (!embeddingModelProviders || Object.keys(embeddingModelProviders).length === 0)
|
||||
return toast.error("No embedding models available");
|
||||
|
||||
chatModelProvider = Object.keys(chatModelProviders)[0];
|
||||
chatModel = Object.keys(chatModelProviders[chatModelProvider])[0];
|
||||
|
||||
embeddingModelProvider = Object.keys(embeddingModelProviders)[0];
|
||||
embeddingModel = Object.keys(
|
||||
embeddingModelProviders[embeddingModelProvider],
|
||||
)[0];
|
||||
embeddingModel = Object.keys(embeddingModelProviders[embeddingModelProvider])[0];
|
||||
|
||||
localStorage.setItem('chatModel', chatModel!);
|
||||
localStorage.setItem('chatModelProvider', chatModelProvider);
|
||||
localStorage.setItem('embeddingModel', embeddingModel!);
|
||||
localStorage.setItem(
|
||||
'embeddingModelProvider',
|
||||
embeddingModelProvider,
|
||||
);
|
||||
localStorage.setItem("chatModel", chatModel!);
|
||||
localStorage.setItem("chatModelProvider", chatModelProvider);
|
||||
localStorage.setItem("embeddingModel", embeddingModel!);
|
||||
localStorage.setItem("embeddingModelProvider", embeddingModelProvider);
|
||||
}
|
||||
|
||||
const wsURL = new URL(url);
|
||||
const searchParams = new URLSearchParams({});
|
||||
|
||||
searchParams.append('chatModel', chatModel!);
|
||||
searchParams.append('chatModelProvider', chatModelProvider);
|
||||
searchParams.append("chatModel", chatModel!);
|
||||
searchParams.append("chatModelProvider", chatModelProvider);
|
||||
|
||||
if (chatModelProvider === 'custom_openai') {
|
||||
searchParams.append(
|
||||
'openAIApiKey',
|
||||
localStorage.getItem('openAIApiKey')!,
|
||||
);
|
||||
searchParams.append(
|
||||
'openAIBaseURL',
|
||||
localStorage.getItem('openAIBaseURL')!,
|
||||
);
|
||||
if (chatModelProvider === "custom_openai") {
|
||||
searchParams.append("openAIApiKey", localStorage.getItem("openAIApiKey")!);
|
||||
searchParams.append("openAIBaseURL", localStorage.getItem("openAIBaseURL")!);
|
||||
}
|
||||
|
||||
searchParams.append('embeddingModel', embeddingModel!);
|
||||
searchParams.append('embeddingModelProvider', embeddingModelProvider);
|
||||
searchParams.append("embeddingModel", embeddingModel!);
|
||||
searchParams.append("embeddingModelProvider", embeddingModelProvider);
|
||||
|
||||
wsURL.search = searchParams.toString();
|
||||
|
||||
|
@ -114,14 +83,12 @@ const useSocket = (
|
|||
if (ws.readyState !== 1) {
|
||||
ws.close();
|
||||
setError(true);
|
||||
toast.error(
|
||||
'Failed to connect to the server. Please try again later.',
|
||||
);
|
||||
toast.error("Failed to connect to the server. Please try again later.");
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[DEBUG] open');
|
||||
console.log("[DEBUG] open");
|
||||
clearTimeout(timeoutId);
|
||||
setError(false);
|
||||
setIsWSReady(true);
|
||||
|
@ -130,13 +97,13 @@ const useSocket = (
|
|||
ws.onerror = () => {
|
||||
clearTimeout(timeoutId);
|
||||
setError(true);
|
||||
toast.error('WebSocket connection error.');
|
||||
toast.error("WebSocket connection error.");
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
clearTimeout(timeoutId);
|
||||
setError(true);
|
||||
console.log('[DEBUG] closed');
|
||||
console.log("[DEBUG] closed");
|
||||
};
|
||||
|
||||
setWs(ws);
|
||||
|
@ -147,7 +114,7 @@ const useSocket = (
|
|||
|
||||
return () => {
|
||||
ws?.close();
|
||||
console.log('[DEBUG] closed');
|
||||
console.log("[DEBUG] closed");
|
||||
};
|
||||
}, [ws, url, setIsWSReady, setError]);
|
||||
|
||||
|
@ -162,15 +129,12 @@ const loadMessages = async (
|
|||
setFocusMode: (mode: string) => void,
|
||||
setNotFound: (notFound: boolean) => void,
|
||||
) => {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (res.status === 404) {
|
||||
setNotFound(true);
|
||||
|
@ -189,11 +153,11 @@ const loadMessages = async (
|
|||
|
||||
setMessages(messages);
|
||||
|
||||
const history = messages.map((msg) => {
|
||||
const history = messages.map(msg => {
|
||||
return [msg.role, msg.content];
|
||||
}) as [string, string][];
|
||||
|
||||
console.log('[DEBUG] messages loaded');
|
||||
console.log("[DEBUG] messages loaded");
|
||||
|
||||
document.title = messages[0].content;
|
||||
|
||||
|
@ -204,7 +168,7 @@ const loadMessages = async (
|
|||
|
||||
const ChatWindow = ({ id }: { id?: string }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const initialMessage = searchParams.get('q');
|
||||
const initialMessage = searchParams.get("q");
|
||||
|
||||
const [chatId, setChatId] = useState<string | undefined>(id);
|
||||
const [newChatCreated, setNewChatCreated] = useState(false);
|
||||
|
@ -213,11 +177,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
const [isWSReady, setIsWSReady] = useState(false);
|
||||
const ws = useSocket(
|
||||
process.env.NEXT_PUBLIC_WS_URL!,
|
||||
setIsWSReady,
|
||||
setHasError,
|
||||
);
|
||||
const ws = useSocket(process.env.NEXT_PUBLIC_WS_URL!, setIsWSReady, setHasError);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [messageAppeared, setMessageAppeared] = useState(false);
|
||||
|
@ -225,31 +185,19 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
const [chatHistory, setChatHistory] = useState<[string, string][]>([]);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
|
||||
const [focusMode, setFocusMode] = useState('webSearch');
|
||||
const [focusMode, setFocusMode] = useState("webSearch");
|
||||
|
||||
const [isMessagesLoaded, setIsMessagesLoaded] = useState(false);
|
||||
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
chatId &&
|
||||
!newChatCreated &&
|
||||
!isMessagesLoaded &&
|
||||
messages.length === 0
|
||||
) {
|
||||
loadMessages(
|
||||
chatId,
|
||||
setMessages,
|
||||
setIsMessagesLoaded,
|
||||
setChatHistory,
|
||||
setFocusMode,
|
||||
setNotFound,
|
||||
);
|
||||
if (chatId && !newChatCreated && !isMessagesLoaded && messages.length === 0) {
|
||||
loadMessages(chatId, setMessages, setIsMessagesLoaded, setChatHistory, setFocusMode, setNotFound);
|
||||
} else if (!chatId) {
|
||||
setNewChatCreated(true);
|
||||
setIsMessagesLoaded(true);
|
||||
setChatId(crypto.randomBytes(20).toString('hex'));
|
||||
setChatId(crypto.randomBytes(20).toString("hex"));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
@ -272,30 +220,30 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
setMessageAppeared(false);
|
||||
|
||||
let sources: Document[] | undefined = undefined;
|
||||
let recievedMessage = '';
|
||||
let recievedMessage = "";
|
||||
let added = false;
|
||||
|
||||
const messageId = crypto.randomBytes(7).toString('hex');
|
||||
const messageId = crypto.randomBytes(7).toString("hex");
|
||||
|
||||
ws?.send(
|
||||
JSON.stringify({
|
||||
type: 'message',
|
||||
type: "message",
|
||||
message: {
|
||||
chatId: chatId!,
|
||||
content: message,
|
||||
},
|
||||
focusMode: focusMode,
|
||||
history: [...chatHistory, ['human', message]],
|
||||
history: [...chatHistory, ["human", message]],
|
||||
}),
|
||||
);
|
||||
|
||||
setMessages((prevMessages) => [
|
||||
setMessages(prevMessages => [
|
||||
...prevMessages,
|
||||
{
|
||||
content: message,
|
||||
messageId: messageId,
|
||||
chatId: chatId!,
|
||||
role: 'user',
|
||||
role: "user",
|
||||
createdAt: new Date(),
|
||||
},
|
||||
]);
|
||||
|
@ -303,22 +251,22 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
const messageHandler = async (e: MessageEvent) => {
|
||||
const data = JSON.parse(e.data);
|
||||
|
||||
if (data.type === 'error') {
|
||||
if (data.type === "error") {
|
||||
toast.error(data.data);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'sources') {
|
||||
if (data.type === "sources") {
|
||||
sources = data.data;
|
||||
if (!added) {
|
||||
setMessages((prevMessages) => [
|
||||
setMessages(prevMessages => [
|
||||
...prevMessages,
|
||||
{
|
||||
content: '',
|
||||
content: "",
|
||||
messageId: data.messageId,
|
||||
chatId: chatId!,
|
||||
role: 'assistant',
|
||||
role: "assistant",
|
||||
sources: sources,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
|
@ -328,15 +276,15 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
setMessageAppeared(true);
|
||||
}
|
||||
|
||||
if (data.type === 'message') {
|
||||
if (data.type === "message") {
|
||||
if (!added) {
|
||||
setMessages((prevMessages) => [
|
||||
setMessages(prevMessages => [
|
||||
...prevMessages,
|
||||
{
|
||||
content: data.data,
|
||||
messageId: data.messageId,
|
||||
chatId: chatId!,
|
||||
role: 'assistant',
|
||||
role: "assistant",
|
||||
sources: sources,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
|
@ -344,8 +292,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
added = true;
|
||||
}
|
||||
|
||||
setMessages((prev) =>
|
||||
prev.map((message) => {
|
||||
setMessages(prev =>
|
||||
prev.map(message => {
|
||||
if (message.messageId === data.messageId) {
|
||||
return { ...message, content: message.content + data.data };
|
||||
}
|
||||
|
@ -358,27 +306,18 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
setMessageAppeared(true);
|
||||
}
|
||||
|
||||
if (data.type === 'messageEnd') {
|
||||
setChatHistory((prevHistory) => [
|
||||
...prevHistory,
|
||||
['human', message],
|
||||
['assistant', recievedMessage],
|
||||
]);
|
||||
if (data.type === "messageEnd") {
|
||||
setChatHistory(prevHistory => [...prevHistory, ["human", message], ["assistant", recievedMessage]]);
|
||||
|
||||
ws?.removeEventListener('message', messageHandler);
|
||||
ws?.removeEventListener("message", messageHandler);
|
||||
setLoading(false);
|
||||
|
||||
const lastMsg = messagesRef.current[messagesRef.current.length - 1];
|
||||
|
||||
if (
|
||||
lastMsg.role === 'assistant' &&
|
||||
lastMsg.sources &&
|
||||
lastMsg.sources.length > 0 &&
|
||||
!lastMsg.suggestions
|
||||
) {
|
||||
if (lastMsg.role === "assistant" && lastMsg.sources && lastMsg.sources.length > 0 && !lastMsg.suggestions) {
|
||||
const suggestions = await getSuggestions(messagesRef.current);
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) => {
|
||||
setMessages(prev =>
|
||||
prev.map(msg => {
|
||||
if (msg.messageId === lastMsg.messageId) {
|
||||
return { ...msg, suggestions: suggestions };
|
||||
}
|
||||
|
@ -389,20 +328,20 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
}
|
||||
};
|
||||
|
||||
ws?.addEventListener('message', messageHandler);
|
||||
ws?.addEventListener("message", messageHandler);
|
||||
};
|
||||
|
||||
const rewrite = (messageId: string) => {
|
||||
const index = messages.findIndex((msg) => msg.messageId === messageId);
|
||||
const index = messages.findIndex(msg => msg.messageId === messageId);
|
||||
|
||||
if (index === -1) return;
|
||||
|
||||
const message = messages[index - 1];
|
||||
|
||||
setMessages((prev) => {
|
||||
setMessages(prev => {
|
||||
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
|
||||
});
|
||||
setChatHistory((prev) => {
|
||||
setChatHistory(prev => {
|
||||
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
|
||||
});
|
||||
|
||||
|
@ -443,11 +382,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||
/>
|
||||
</>
|
||||
) : (
|
||||
<EmptyChat
|
||||
sendMessage={sendMessage}
|
||||
focusMode={focusMode}
|
||||
setFocusMode={setFocusMode}
|
||||
/>
|
||||
<EmptyChat sendMessage={sendMessage} focusMode={focusMode} setFocusMode={setFocusMode} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Delete, Trash } from 'lucide-react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Chat } from '@/app/library/page';
|
||||
import { Delete, Trash } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Fragment, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Chat } from "@/app/library/page";
|
||||
|
||||
const DeleteChat = ({
|
||||
chatId,
|
||||
|
@ -19,21 +19,18 @@ const DeleteChat = ({
|
|||
const handleDelete = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (res.status != 200) {
|
||||
throw new Error('Failed to delete chat');
|
||||
throw new Error("Failed to delete chat");
|
||||
}
|
||||
|
||||
const newChats = chats.filter((chat) => chat.id !== chatId);
|
||||
const newChats = chats.filter(chat => chat.id !== chatId);
|
||||
|
||||
setChats(newChats);
|
||||
} catch (err: any) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import EmptyChatMessageInput from './EmptyChatMessageInput';
|
||||
import EmptyChatMessageInput from "./EmptyChatMessageInput";
|
||||
|
||||
const EmptyChat = ({
|
||||
sendMessage,
|
||||
|
@ -12,14 +12,8 @@ const EmptyChat = ({
|
|||
return (
|
||||
<div className="relative">
|
||||
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-8">
|
||||
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
|
||||
Research begins here.
|
||||
</h2>
|
||||
<EmptyChatMessageInput
|
||||
sendMessage={sendMessage}
|
||||
focusMode={focusMode}
|
||||
setFocusMode={setFocusMode}
|
||||
/>
|
||||
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">Research begins here.</h2>
|
||||
<EmptyChatMessageInput sendMessage={sendMessage} focusMode={focusMode} setFocusMode={setFocusMode} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
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 { 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";
|
||||
|
||||
const EmptyChatMessageInput = ({
|
||||
sendMessage,
|
||||
|
@ -14,37 +14,37 @@ const EmptyChatMessageInput = ({
|
|||
setFocusMode: (mode: string) => void;
|
||||
}) => {
|
||||
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === '/') {
|
||||
if (e.key === "/") {
|
||||
e.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
sendMessage(message);
|
||||
setMessage('');
|
||||
setMessage("");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage(message);
|
||||
setMessage('');
|
||||
setMessage("");
|
||||
}
|
||||
}}
|
||||
className="w-full"
|
||||
|
@ -53,7 +53,7 @@ const EmptyChatMessageInput = ({
|
|||
<TextareaAutosize
|
||||
ref={inputRef}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
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..."
|
||||
|
@ -64,10 +64,7 @@ const EmptyChatMessageInput = ({
|
|||
{/* <Attach /> */}
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-4 -mx-2">
|
||||
<CopilotToggle
|
||||
copilotEnabled={copilotEnabled}
|
||||
setCopilotEnabled={setCopilotEnabled}
|
||||
/>
|
||||
<CopilotToggle copilotEnabled={copilotEnabled} setCopilotEnabled={setCopilotEnabled} />
|
||||
<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"
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
import { Check, ClipboardList } from 'lucide-react';
|
||||
import { Message } from '../ChatWindow';
|
||||
import { useState } from 'react';
|
||||
import { Check, ClipboardList } from "lucide-react";
|
||||
import { Message } from "../ChatWindow";
|
||||
import { useState } from "react";
|
||||
|
||||
const Copy = ({
|
||||
message,
|
||||
initialMessage,
|
||||
}: {
|
||||
message: Message;
|
||||
initialMessage: string;
|
||||
}) => {
|
||||
const Copy = ({ message, initialMessage }: { message: Message; initialMessage: string }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
import { ArrowLeftRight } from 'lucide-react';
|
||||
import { ArrowLeftRight } from "lucide-react";
|
||||
|
||||
const Rewrite = ({
|
||||
rewrite,
|
||||
messageId,
|
||||
}: {
|
||||
rewrite: (messageId: string) => void;
|
||||
messageId: string;
|
||||
}) => {
|
||||
const Rewrite = ({ rewrite, messageId }: { rewrite: (messageId: string) => void; messageId: string }) => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => rewrite(messageId)}
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import React, { MutableRefObject, useEffect, useState } from 'react';
|
||||
import { Message } from './ChatWindow';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
BookCopy,
|
||||
Disc3,
|
||||
Volume2,
|
||||
StopCircle,
|
||||
Layers3,
|
||||
Plus,
|
||||
} from 'lucide-react';
|
||||
import Markdown from 'markdown-to-jsx';
|
||||
import Copy from './MessageActions/Copy';
|
||||
import Rewrite from './MessageActions/Rewrite';
|
||||
import MessageSources from './MessageSources';
|
||||
import SearchImages from './SearchImages';
|
||||
import SearchVideos from './SearchVideos';
|
||||
import { useSpeech } from 'react-text-to-speech';
|
||||
import React, { MutableRefObject, useEffect, useState } from "react";
|
||||
import { Message } from "./ChatWindow";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { BookCopy, Disc3, Volume2, StopCircle, Layers3, Plus } from "lucide-react";
|
||||
import Markdown from "markdown-to-jsx";
|
||||
import Copy from "./MessageActions/Copy";
|
||||
import Rewrite from "./MessageActions/Rewrite";
|
||||
import MessageSources from "./MessageSources";
|
||||
import SearchImages from "./SearchImages";
|
||||
import SearchVideos from "./SearchVideos";
|
||||
import { useSpeech } from "react-text-to-speech";
|
||||
|
||||
const MessageBox = ({
|
||||
message,
|
||||
|
@ -45,11 +38,7 @@ const MessageBox = ({
|
|||
useEffect(() => {
|
||||
const regex = /\[(\d+)\]/g;
|
||||
|
||||
if (
|
||||
message.role === 'assistant' &&
|
||||
message?.sources &&
|
||||
message.sources.length > 0
|
||||
) {
|
||||
if (message.role === "assistant" && message?.sources && message.sources.length > 0) {
|
||||
return setParsedMessage(
|
||||
message.content.replace(
|
||||
regex,
|
||||
|
@ -59,7 +48,7 @@ const MessageBox = ({
|
|||
);
|
||||
}
|
||||
|
||||
setSpeechMessage(message.content.replace(regex, ''));
|
||||
setSpeechMessage(message.content.replace(regex, ""));
|
||||
setParsedMessage(message.content);
|
||||
}, [message.content, message.sources, message.role]);
|
||||
|
||||
|
@ -67,27 +56,20 @@ const MessageBox = ({
|
|||
|
||||
return (
|
||||
<div>
|
||||
{message.role === 'user' && (
|
||||
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8')}>
|
||||
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
|
||||
{message.content}
|
||||
</h2>
|
||||
{message.role === "user" && (
|
||||
<div className={cn("w-full", messageIndex === 0 ? "pt-16" : "pt-8")}>
|
||||
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">{message.content}</h2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message.role === 'assistant' && (
|
||||
{message.role === "assistant" && (
|
||||
<div className="flex flex-col space-y-9 lg:space-y-0 lg:flex-row lg:justify-between lg:space-x-9">
|
||||
<div
|
||||
ref={dividerRef}
|
||||
className="flex flex-col space-y-6 w-full lg:w-9/12"
|
||||
>
|
||||
<div ref={dividerRef} className="flex flex-col space-y-6 w-full lg:w-9/12">
|
||||
{message.sources && message.sources.length > 0 && (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<BookCopy className="text-black dark:text-white" size={20} />
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||
Sources
|
||||
</h3>
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">Sources</h3>
|
||||
</div>
|
||||
<MessageSources sources={message.sources} />
|
||||
</div>
|
||||
|
@ -95,20 +77,15 @@ const MessageBox = ({
|
|||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Disc3
|
||||
className={cn(
|
||||
'text-black dark:text-white',
|
||||
isLast && loading ? 'animate-spin' : 'animate-none',
|
||||
)}
|
||||
className={cn("text-black dark:text-white", isLast && loading ? "animate-spin" : "animate-none")}
|
||||
size={20}
|
||||
/>
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||
Answer
|
||||
</h3>
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">Answer</h3>
|
||||
</div>
|
||||
<Markdown
|
||||
className={cn(
|
||||
'prose dark:prose-invert prose-p:leading-relaxed prose-pre:p-0',
|
||||
'max-w-none break-words text-black dark:text-white text-sm md:text-base font-medium',
|
||||
"prose dark:prose-invert prose-p:leading-relaxed prose-pre:p-0",
|
||||
"max-w-none break-words text-black dark:text-white text-sm md:text-base font-medium",
|
||||
)}
|
||||
>
|
||||
{parsedMessage}
|
||||
|
@ -125,7 +102,7 @@ const MessageBox = ({
|
|||
<Copy initialMessage={message.content} message={message} />
|
||||
<button
|
||||
onClick={() => {
|
||||
if (speechStatus === 'started') {
|
||||
if (speechStatus === "started") {
|
||||
stop();
|
||||
} else {
|
||||
start();
|
||||
|
@ -133,11 +110,7 @@ const MessageBox = ({
|
|||
}}
|
||||
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
||||
>
|
||||
{speechStatus === 'started' ? (
|
||||
<StopCircle size={18} />
|
||||
) : (
|
||||
<Volume2 size={18} />
|
||||
)}
|
||||
{speechStatus === "started" ? <StopCircle size={18} /> : <Volume2 size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -145,7 +118,7 @@ const MessageBox = ({
|
|||
{isLast &&
|
||||
message.suggestions &&
|
||||
message.suggestions.length > 0 &&
|
||||
message.role === 'assistant' &&
|
||||
message.role === "assistant" &&
|
||||
!loading && (
|
||||
<>
|
||||
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||
|
@ -156,10 +129,7 @@ const MessageBox = ({
|
|||
</div>
|
||||
<div className="flex flex-col space-y-3">
|
||||
{message.suggestions.map((suggestion, i) => (
|
||||
<div
|
||||
className="flex flex-col space-y-3 text-sm"
|
||||
key={i}
|
||||
>
|
||||
<div className="flex flex-col space-y-3 text-sm" key={i}>
|
||||
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||
<div
|
||||
onClick={() => {
|
||||
|
@ -167,13 +137,8 @@ const MessageBox = ({
|
|||
}}
|
||||
className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center"
|
||||
>
|
||||
<p className="transition duration-200 hover:text-[#24A0ED]">
|
||||
{suggestion}
|
||||
</p>
|
||||
<Plus
|
||||
size={20}
|
||||
className="text-[#24A0ED] flex-shrink-0"
|
||||
/>
|
||||
<p className="transition duration-200 hover:text-[#24A0ED]">{suggestion}</p>
|
||||
<Plus size={20} className="text-[#24A0ED] flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
@ -184,14 +149,8 @@ const MessageBox = ({
|
|||
</div>
|
||||
</div>
|
||||
<div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4">
|
||||
<SearchImages
|
||||
query={history[messageIndex - 1].content}
|
||||
chat_history={history.slice(0, messageIndex - 1)}
|
||||
/>
|
||||
<SearchVideos
|
||||
chat_history={history.slice(0, messageIndex - 1)}
|
||||
query={history[messageIndex - 1].content}
|
||||
/>
|
||||
<SearchImages query={history[messageIndex - 1].content} chat_history={history.slice(0, messageIndex - 1)} />
|
||||
<SearchVideos chat_history={history.slice(0, messageIndex - 1)} query={history[messageIndex - 1].content} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,84 +1,75 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import { ArrowUp } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import Attach from './MessageInputActions/Attach';
|
||||
import CopilotToggle from './MessageInputActions/Copilot';
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowUp } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import Attach from "./MessageInputActions/Attach";
|
||||
import CopilotToggle from "./MessageInputActions/Copilot";
|
||||
|
||||
const MessageInput = ({
|
||||
sendMessage,
|
||||
loading,
|
||||
}: {
|
||||
sendMessage: (message: string) => void;
|
||||
loading: boolean;
|
||||
}) => {
|
||||
const MessageInput = ({ sendMessage, loading }: { sendMessage: (message: string) => void; loading: boolean }) => {
|
||||
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [message, setMessage] = useState("");
|
||||
const [textareaRows, setTextareaRows] = useState(1);
|
||||
const [mode, setMode] = useState<'multi' | 'single'>('single');
|
||||
const [mode, setMode] = useState<"multi" | "single">("single");
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRows >= 2 && message && mode === 'single') {
|
||||
setMode('multi');
|
||||
} else if (!message && mode === 'multi') {
|
||||
setMode('single');
|
||||
if (textareaRows >= 2 && message && mode === "single") {
|
||||
setMode("multi");
|
||||
} else if (!message && mode === "multi") {
|
||||
setMode("single");
|
||||
}
|
||||
}, [textareaRows, mode, message]);
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === '/') {
|
||||
if (e.key === "/") {
|
||||
e.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
onSubmit={e => {
|
||||
if (loading) return;
|
||||
e.preventDefault();
|
||||
sendMessage(message);
|
||||
setMessage('');
|
||||
setMessage("");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !loading) {
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" && !e.shiftKey && !loading) {
|
||||
e.preventDefault();
|
||||
sendMessage(message);
|
||||
setMessage('');
|
||||
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',
|
||||
mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full',
|
||||
"bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200",
|
||||
mode === "multi" ? "flex-col rounded-lg" : "flex-row rounded-full",
|
||||
)}
|
||||
>
|
||||
{mode === 'single' && <Attach />}
|
||||
{mode === "single" && <Attach />}
|
||||
<TextareaAutosize
|
||||
ref={inputRef}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
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' && (
|
||||
{mode === "single" && (
|
||||
<div className="flex flex-row items-center space-x-4">
|
||||
<CopilotToggle
|
||||
copilotEnabled={copilotEnabled}
|
||||
setCopilotEnabled={setCopilotEnabled}
|
||||
/>
|
||||
<CopilotToggle copilotEnabled={copilotEnabled} setCopilotEnabled={setCopilotEnabled} />
|
||||
<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"
|
||||
|
@ -87,14 +78,11 @@ const MessageInput = ({
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
{mode === 'multi' && (
|
||||
{mode === "multi" && (
|
||||
<div className="flex flex-row items-center justify-between w-full pt-2">
|
||||
<Attach />
|
||||
<div className="flex flex-row items-center space-x-4">
|
||||
<CopilotToggle
|
||||
copilotEnabled={copilotEnabled}
|
||||
setCopilotEnabled={setCopilotEnabled}
|
||||
/>
|
||||
<CopilotToggle copilotEnabled={copilotEnabled} setCopilotEnabled={setCopilotEnabled} />
|
||||
<button
|
||||
disabled={message.trim().length === 0 || loading}
|
||||
className="bg-[#24A0ED] text-white 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"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CopyPlus } from 'lucide-react';
|
||||
import { CopyPlus } from "lucide-react";
|
||||
|
||||
const Attach = () => {
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import { Switch } from '@headlessui/react';
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
const CopilotToggle = ({
|
||||
copilotEnabled,
|
||||
|
@ -18,20 +18,18 @@ const CopilotToggle = ({
|
|||
<span className="sr-only">Copilot</span>
|
||||
<span
|
||||
className={cn(
|
||||
copilotEnabled
|
||||
? 'translate-x-6 bg-[#24A0ED]'
|
||||
: 'translate-x-1 bg-black/50 dark:bg-white/50',
|
||||
'inline-block h-3 w-3 sm:h-4 sm:w-4 transform rounded-full transition-all duration-200',
|
||||
copilotEnabled ? "translate-x-6 bg-[#24A0ED]" : "translate-x-1 bg-black/50 dark:bg-white/50",
|
||||
"inline-block h-3 w-3 sm:h-4 sm:w-4 transform rounded-full transition-all duration-200",
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<p
|
||||
onClick={() => setCopilotEnabled(!copilotEnabled)}
|
||||
className={cn(
|
||||
'text-xs font-medium transition-colors duration-150 ease-in-out',
|
||||
"text-xs font-medium transition-colors duration-150 ease-in-out",
|
||||
copilotEnabled
|
||||
? 'text-[#24A0ED]'
|
||||
: 'text-black/50 dark:text-white/50 group-hover:text-black dark:group-hover:text-white',
|
||||
? "text-[#24A0ED]"
|
||||
: "text-black/50 dark:text-white/50 group-hover:text-black dark:group-hover:text-white",
|
||||
)}
|
||||
>
|
||||
Copilot
|
||||
|
|
|
@ -1,86 +1,63 @@
|
|||
import {
|
||||
BadgePercent,
|
||||
ChevronDown,
|
||||
Globe,
|
||||
Pencil,
|
||||
ScanEye,
|
||||
SwatchBook,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Popover, Transition } from '@headlessui/react';
|
||||
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
|
||||
import { Fragment } from 'react';
|
||||
import { BadgePercent, ChevronDown, Globe, Pencil, ScanEye, SwatchBook } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { SiReddit, SiYoutube } from "@icons-pack/react-simple-icons";
|
||||
import { Fragment } from "react";
|
||||
|
||||
const focusModes = [
|
||||
{
|
||||
key: 'webSearch',
|
||||
title: 'All',
|
||||
description: 'Searches across all of the internet',
|
||||
key: "webSearch",
|
||||
title: "All",
|
||||
description: "Searches across all of the internet",
|
||||
icon: <Globe size={20} />,
|
||||
},
|
||||
{
|
||||
key: 'academicSearch',
|
||||
title: 'Academic',
|
||||
description: 'Search in published academic papers',
|
||||
key: "academicSearch",
|
||||
title: "Academic",
|
||||
description: "Search in published academic papers",
|
||||
icon: <SwatchBook size={20} />,
|
||||
},
|
||||
{
|
||||
key: 'writingAssistant',
|
||||
title: 'Writing',
|
||||
description: 'Chat without searching the web',
|
||||
key: "writingAssistant",
|
||||
title: "Writing",
|
||||
description: "Chat without searching the web",
|
||||
icon: <Pencil size={16} />,
|
||||
},
|
||||
{
|
||||
key: 'wolframAlphaSearch',
|
||||
title: 'Wolfram Alpha',
|
||||
description: 'Computational knowledge engine',
|
||||
key: "wolframAlphaSearch",
|
||||
title: "Wolfram Alpha",
|
||||
description: "Computational knowledge engine",
|
||||
icon: <BadgePercent size={20} />,
|
||||
},
|
||||
{
|
||||
key: 'youtubeSearch',
|
||||
title: 'Youtube',
|
||||
description: 'Search and watch videos',
|
||||
key: "youtubeSearch",
|
||||
title: "Youtube",
|
||||
description: "Search and watch videos",
|
||||
icon: (
|
||||
<SiYoutube
|
||||
className="h-5 w-auto mr-0.5"
|
||||
onPointerEnterCapture={undefined}
|
||||
onPointerLeaveCapture={undefined}
|
||||
/>
|
||||
<SiYoutube className="h-5 w-auto mr-0.5" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'redditSearch',
|
||||
title: 'Reddit',
|
||||
description: 'Search for discussions and opinions',
|
||||
key: "redditSearch",
|
||||
title: "Reddit",
|
||||
description: "Search for discussions and opinions",
|
||||
icon: (
|
||||
<SiReddit
|
||||
className="h-5 w-auto mr-0.5"
|
||||
onPointerEnterCapture={undefined}
|
||||
onPointerLeaveCapture={undefined}
|
||||
/>
|
||||
<SiReddit className="h-5 w-auto mr-0.5" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const Focus = ({
|
||||
focusMode,
|
||||
setFocusMode,
|
||||
}: {
|
||||
focusMode: string;
|
||||
setFocusMode: (mode: string) => void;
|
||||
}) => {
|
||||
const Focus = ({ focusMode, setFocusMode }: { focusMode: string; setFocusMode: (mode: string) => void }) => {
|
||||
return (
|
||||
<Popover className="fixed w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
||||
<Popover.Button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
{focusMode !== 'webSearch' ? (
|
||||
{focusMode !== "webSearch" ? (
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
{focusModes.find((mode) => mode.key === focusMode)?.icon}
|
||||
<p className="text-xs font-medium">
|
||||
{focusModes.find((mode) => mode.key === focusMode)?.title}
|
||||
</p>
|
||||
{focusModes.find(mode => mode.key === focusMode)?.icon}
|
||||
<p className="text-xs font-medium">{focusModes.find(mode => mode.key === focusMode)?.title}</p>
|
||||
<ChevronDown size={20} />
|
||||
</div>
|
||||
) : (
|
||||
|
@ -103,26 +80,22 @@ const Focus = ({
|
|||
onClick={() => setFocusMode(mode.key)}
|
||||
key={i}
|
||||
className={cn(
|
||||
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition',
|
||||
"p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition",
|
||||
focusMode === mode.key
|
||||
? 'bg-light-secondary dark:bg-dark-secondary'
|
||||
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
|
||||
? "bg-light-secondary dark:bg-dark-secondary"
|
||||
: "hover:bg-light-secondary dark:hover:bg-dark-secondary",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-row items-center space-x-1',
|
||||
focusMode === mode.key
|
||||
? 'text-[#24A0ED]'
|
||||
: 'text-black dark:text-white',
|
||||
"flex flex-row items-center space-x-1",
|
||||
focusMode === mode.key ? "text-[#24A0ED]" : "text-black dark:text-white",
|
||||
)}
|
||||
>
|
||||
{mode.icon}
|
||||
<p className="text-sm font-medium">{mode.title}</p>
|
||||
</div>
|
||||
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||
{mode.description}
|
||||
</p>
|
||||
<p className="text-black/70 dark:text-white/70 text-xs">{mode.description}</p>
|
||||
</Popover.Button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
/* eslint-disable @next/next/no-img-element */
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { Document } from '@langchain/core/documents';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Document } from "@langchain/core/documents";
|
||||
import { Fragment, useState } from "react";
|
||||
|
||||
const MessageSources = ({ sources }: { sources: Document[] }) => {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
const closeModal = () => {
|
||||
setIsDialogOpen(false);
|
||||
document.body.classList.remove('overflow-hidden-scrollable');
|
||||
document.body.classList.remove("overflow-hidden-scrollable");
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
setIsDialogOpen(true);
|
||||
document.body.classList.add('overflow-hidden-scrollable');
|
||||
document.body.classList.add("overflow-hidden-scrollable");
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -38,7 +38,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
|||
className="rounded-lg h-4 w-4"
|
||||
/>
|
||||
<p className="text-xs text-black/50 dark:text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{source.metadata.url.replace(/.+\/\/|www.|\..+/g, '')}
|
||||
{source.metadata.url.replace(/.+\/\/|www.|\..+/g, "")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs">
|
||||
|
@ -65,9 +65,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-black/50 dark:text-white/50">
|
||||
View {sources.length - 3} more
|
||||
</p>
|
||||
<p className="text-xs text-black/50 dark:text-white/50">View {sources.length - 3} more</p>
|
||||
</button>
|
||||
)}
|
||||
<Transition appear show={isDialogOpen} as={Fragment}>
|
||||
|
@ -84,9 +82,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
|||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title className="text-lg font-medium leading-6 dark:text-white">
|
||||
Sources
|
||||
</Dialog.Title>
|
||||
<Dialog.Title className="text-lg font-medium leading-6 dark:text-white">Sources</Dialog.Title>
|
||||
<div className="grid grid-cols-2 gap-2 overflow-auto max-h-[300px] mt-2 pr-2">
|
||||
{sources.map((source, i) => (
|
||||
<a
|
||||
|
@ -108,10 +104,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
|||
className="rounded-lg h-4 w-4"
|
||||
/>
|
||||
<p className="text-xs text-black/50 dark:text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{source.metadata.url.replace(
|
||||
/.+\/\/|www.|\..+/g,
|
||||
'',
|
||||
)}
|
||||
{source.metadata.url.replace(/.+\/\/|www.|\..+/g, "")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs">
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
import { Clock, Edit, Share, Trash } from 'lucide-react';
|
||||
import { Message } from './ChatWindow';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { formatTimeDifference } from '@/lib/utils';
|
||||
import { Clock, Edit, Share, Trash } from "lucide-react";
|
||||
import { Message } from "./ChatWindow";
|
||||
import { useEffect, useState } from "react";
|
||||
import { formatTimeDifference } from "@/lib/utils";
|
||||
|
||||
const Navbar = ({ messages }: { messages: Message[] }) => {
|
||||
const [title, setTitle] = useState<string>('');
|
||||
const [timeAgo, setTimeAgo] = useState<string>('');
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [timeAgo, setTimeAgo] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
const newTitle =
|
||||
messages[0].content.length > 20
|
||||
? `${messages[0].content.substring(0, 20).trim()}...`
|
||||
: messages[0].content;
|
||||
messages[0].content.length > 20 ? `${messages[0].content.substring(0, 20).trim()}...` : messages[0].content;
|
||||
setTitle(newTitle);
|
||||
const newTimeAgo = formatTimeDifference(
|
||||
new Date(),
|
||||
messages[0].createdAt,
|
||||
);
|
||||
const newTimeAgo = formatTimeDifference(new Date(), messages[0].createdAt);
|
||||
setTimeAgo(newTimeAgo);
|
||||
}
|
||||
}, [messages]);
|
||||
|
@ -25,10 +20,7 @@ const Navbar = ({ messages }: { messages: Message[] }) => {
|
|||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
if (messages.length > 0) {
|
||||
const newTimeAgo = formatTimeDifference(
|
||||
new Date(),
|
||||
messages[0].createdAt,
|
||||
);
|
||||
const newTimeAgo = formatTimeDifference(new Date(), messages[0].createdAt);
|
||||
setTimeAgo(newTimeAgo);
|
||||
}
|
||||
}, 1000);
|
||||
|
@ -39,10 +31,7 @@ const Navbar = ({ messages }: { messages: Message[] }) => {
|
|||
|
||||
return (
|
||||
<div className="fixed z-40 top-0 left-0 right-0 px-4 lg:pl-[104px] lg:pr-6 lg:px-8 flex flex-row items-center justify-between w-full py-4 text-sm text-black dark:text-white/70 border-b bg-light-primary dark:bg-dark-primary border-light-100 dark:border-dark-200">
|
||||
<Edit
|
||||
size={17}
|
||||
className="active:scale-95 transition duration-100 cursor-pointer lg:hidden"
|
||||
/>
|
||||
<Edit size={17} className="active:scale-95 transition duration-100 cursor-pointer lg:hidden" />
|
||||
<div className="hidden lg:flex flex-row items-center justify-center space-x-2">
|
||||
<Clock size={17} />
|
||||
<p className="text-xs">{timeAgo} ago</p>
|
||||
|
@ -50,14 +39,8 @@ const Navbar = ({ messages }: { messages: Message[] }) => {
|
|||
<p className="hidden lg:flex">{title}</p>
|
||||
|
||||
<div className="flex flex-row items-center space-x-4">
|
||||
<Share
|
||||
size={17}
|
||||
className="active:scale-95 transition duration-100 cursor-pointer"
|
||||
/>
|
||||
<Trash
|
||||
size={17}
|
||||
className="text-red-400 active:scale-95 transition duration-100 cursor-pointer"
|
||||
/>
|
||||
<Share size={17} className="active:scale-95 transition duration-100 cursor-pointer" />
|
||||
<Trash size={17} className="text-red-400 active:scale-95 transition duration-100 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/* eslint-disable @next/next/no-img-element */
|
||||
import { ImagesIcon, PlusIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Lightbox from 'yet-another-react-lightbox';
|
||||
import 'yet-another-react-lightbox/styles.css';
|
||||
import { Message } from './ChatWindow';
|
||||
import { ImagesIcon, PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import Lightbox from "yet-another-react-lightbox";
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
import { Message } from "./ChatWindow";
|
||||
|
||||
type Image = {
|
||||
url: string;
|
||||
|
@ -11,13 +11,7 @@ type Image = {
|
|||
title: string;
|
||||
};
|
||||
|
||||
const SearchImages = ({
|
||||
query,
|
||||
chat_history,
|
||||
}: {
|
||||
query: string;
|
||||
chat_history: Message[];
|
||||
}) => {
|
||||
const SearchImages = ({ query, chat_history }: { query: string; chat_history: Message[] }) => {
|
||||
const [images, setImages] = useState<Image[] | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
@ -30,24 +24,21 @@ const SearchImages = ({
|
|||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
const chatModelProvider = localStorage.getItem('chatModelProvider');
|
||||
const chatModel = localStorage.getItem('chatModel');
|
||||
const chatModelProvider = localStorage.getItem("chatModelProvider");
|
||||
const chatModel = localStorage.getItem("chatModel");
|
||||
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/images`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
chat_history: chat_history,
|
||||
chat_model_provider: chatModelProvider,
|
||||
chat_model: chatModel,
|
||||
}),
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/images`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
chat_history: chat_history,
|
||||
chat_model_provider: chatModelProvider,
|
||||
chat_model: chatModel,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
|
@ -89,11 +80,7 @@ const SearchImages = ({
|
|||
<img
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
setSlides([
|
||||
slides[i],
|
||||
...slides.slice(0, i),
|
||||
...slides.slice(i + 1),
|
||||
]);
|
||||
setSlides([slides[i], ...slides.slice(0, i), ...slides.slice(i + 1)]);
|
||||
}}
|
||||
key={i}
|
||||
src={image.img_src}
|
||||
|
@ -105,11 +92,7 @@ const SearchImages = ({
|
|||
<img
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
setSlides([
|
||||
slides[i],
|
||||
...slides.slice(0, i),
|
||||
...slides.slice(i + 1),
|
||||
]);
|
||||
setSlides([slides[i], ...slides.slice(0, i), ...slides.slice(i + 1)]);
|
||||
}}
|
||||
key={i}
|
||||
src={image.img_src}
|
||||
|
@ -132,9 +115,7 @@ const SearchImages = ({
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||
View {images.length - 3} more
|
||||
</p>
|
||||
<p className="text-black/70 dark:text-white/70 text-xs">View {images.length - 3} more</p>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/* eslint-disable @next/next/no-img-element */
|
||||
import { PlayCircle, PlayIcon, PlusIcon, VideoIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
|
||||
import 'yet-another-react-lightbox/styles.css';
|
||||
import { Message } from './ChatWindow';
|
||||
import { PlayCircle, PlayIcon, PlusIcon, VideoIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import Lightbox, { GenericSlide, VideoSlide } from "yet-another-react-lightbox";
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
import { Message } from "./ChatWindow";
|
||||
|
||||
type Video = {
|
||||
url: string;
|
||||
|
@ -12,25 +12,19 @@ type Video = {
|
|||
iframe_src: string;
|
||||
};
|
||||
|
||||
declare module 'yet-another-react-lightbox' {
|
||||
declare module "yet-another-react-lightbox" {
|
||||
export interface VideoSlide extends GenericSlide {
|
||||
type: 'video-slide';
|
||||
type: "video-slide";
|
||||
src: string;
|
||||
iframe_src: string;
|
||||
}
|
||||
|
||||
interface SlideTypes {
|
||||
'video-slide': VideoSlide;
|
||||
"video-slide": VideoSlide;
|
||||
}
|
||||
}
|
||||
|
||||
const Searchvideos = ({
|
||||
query,
|
||||
chat_history,
|
||||
}: {
|
||||
query: string;
|
||||
chat_history: Message[];
|
||||
}) => {
|
||||
const Searchvideos = ({ query, chat_history }: { query: string; chat_history: Message[] }) => {
|
||||
const [videos, setVideos] = useState<Video[] | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
@ -43,24 +37,21 @@ const Searchvideos = ({
|
|||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
const chatModelProvider = localStorage.getItem('chatModelProvider');
|
||||
const chatModel = localStorage.getItem('chatModel');
|
||||
const chatModelProvider = localStorage.getItem("chatModelProvider");
|
||||
const chatModel = localStorage.getItem("chatModel");
|
||||
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/videos`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
chat_history: chat_history,
|
||||
chat_model_provider: chatModelProvider,
|
||||
chat_model: chatModel,
|
||||
}),
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/videos`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
chat_history: chat_history,
|
||||
chat_model_provider: chatModelProvider,
|
||||
chat_model: chatModel,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
|
@ -69,7 +60,7 @@ const Searchvideos = ({
|
|||
setSlides(
|
||||
videos.map((video: Video) => {
|
||||
return {
|
||||
type: 'video-slide',
|
||||
type: "video-slide",
|
||||
iframe_src: video.iframe_src,
|
||||
src: video.img_src,
|
||||
};
|
||||
|
@ -104,11 +95,7 @@ const Searchvideos = ({
|
|||
<div
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
setSlides([
|
||||
slides[i],
|
||||
...slides.slice(0, i),
|
||||
...slides.slice(i + 1),
|
||||
]);
|
||||
setSlides([slides[i], ...slides.slice(0, i), ...slides.slice(i + 1)]);
|
||||
}}
|
||||
className="relative transition duration-200 active:scale-95 hover:scale-[1.02] cursor-pointer"
|
||||
key={i}
|
||||
|
@ -128,11 +115,7 @@ const Searchvideos = ({
|
|||
<div
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
setSlides([
|
||||
slides[i],
|
||||
...slides.slice(0, i),
|
||||
...slides.slice(i + 1),
|
||||
]);
|
||||
setSlides([slides[i], ...slides.slice(0, i), ...slides.slice(i + 1)]);
|
||||
}}
|
||||
className="relative transition duration-200 active:scale-95 hover:scale-[1.02] cursor-pointer"
|
||||
key={i}
|
||||
|
@ -163,9 +146,7 @@ const Searchvideos = ({
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||
View {videos.length - 3} more
|
||||
</p>
|
||||
<p className="text-black/70 dark:text-white/70 text-xs">View {videos.length - 3} more</p>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -175,7 +156,7 @@ const Searchvideos = ({
|
|||
slides={slides}
|
||||
render={{
|
||||
slide: ({ slide }) =>
|
||||
slide.type === 'video-slide' ? (
|
||||
slide.type === "video-slide" ? (
|
||||
<div className="h-full w-full flex flex-row items-center justify-center">
|
||||
<iframe
|
||||
src={slide.iframe_src}
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react';
|
||||
import React, {
|
||||
Fragment,
|
||||
useEffect,
|
||||
useState,
|
||||
type SelectHTMLAttributes,
|
||||
} from 'react';
|
||||
import ThemeSwitcher from './theme/Switcher';
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { CloudUpload, RefreshCcw, RefreshCw } from "lucide-react";
|
||||
import React, { Fragment, useEffect, useState, type SelectHTMLAttributes } from "react";
|
||||
import ThemeSwitcher from "./theme/Switcher";
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
|
@ -16,7 +11,7 @@ const Input = ({ className, ...restProps }: InputProps) => {
|
|||
<input
|
||||
{...restProps}
|
||||
className={cn(
|
||||
'bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm',
|
||||
"bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
|
@ -32,7 +27,7 @@ export const Select = ({ className, options, ...restProps }: SelectProps) => {
|
|||
<select
|
||||
{...restProps}
|
||||
className={cn(
|
||||
'bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm',
|
||||
"bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
@ -59,27 +54,14 @@ interface SettingsType {
|
|||
ollamaApiUrl: string;
|
||||
}
|
||||
|
||||
const SettingsDialog = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}) => {
|
||||
const SettingsDialog = ({ isOpen, setIsOpen }: { isOpen: boolean; setIsOpen: (isOpen: boolean) => void }) => {
|
||||
const [config, setConfig] = useState<SettingsType | null>(null);
|
||||
const [selectedChatModelProvider, setSelectedChatModelProvider] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [selectedChatModel, setSelectedChatModel] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedEmbeddingModelProvider, setSelectedEmbeddingModelProvider] =
|
||||
useState<string | null>(null);
|
||||
const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [customOpenAIApiKey, setCustomOpenAIApiKey] = useState<string>('');
|
||||
const [customOpenAIBaseURL, setCustomOpenAIBaseURL] = useState<string>('');
|
||||
const [selectedChatModelProvider, setSelectedChatModelProvider] = useState<string | null>(null);
|
||||
const [selectedChatModel, setSelectedChatModel] = useState<string | null>(null);
|
||||
const [selectedEmbeddingModelProvider, setSelectedEmbeddingModelProvider] = useState<string | null>(null);
|
||||
const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState<string | null>(null);
|
||||
const [customOpenAIApiKey, setCustomOpenAIApiKey] = useState<string>("");
|
||||
const [customOpenAIBaseURL, setCustomOpenAIBaseURL] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
|
@ -89,52 +71,38 @@ const SettingsDialog = ({
|
|||
setIsLoading(true);
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const data = (await res.json()) as SettingsType;
|
||||
setConfig(data);
|
||||
|
||||
const chatModelProvidersKeys = Object.keys(
|
||||
data.chatModelProviders || {},
|
||||
);
|
||||
const embeddingModelProvidersKeys = Object.keys(
|
||||
data.embeddingModelProviders || {},
|
||||
);
|
||||
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
|
||||
const embeddingModelProvidersKeys = Object.keys(data.embeddingModelProviders || {});
|
||||
|
||||
const defaultChatModelProvider =
|
||||
chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : '';
|
||||
const defaultChatModelProvider = chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : "";
|
||||
const defaultEmbeddingModelProvider =
|
||||
embeddingModelProvidersKeys.length > 0
|
||||
? embeddingModelProvidersKeys[0]
|
||||
: '';
|
||||
embeddingModelProvidersKeys.length > 0 ? embeddingModelProvidersKeys[0] : "";
|
||||
|
||||
const chatModelProvider =
|
||||
localStorage.getItem('chatModelProvider') ||
|
||||
defaultChatModelProvider ||
|
||||
'';
|
||||
const chatModelProvider = localStorage.getItem("chatModelProvider") || defaultChatModelProvider || "";
|
||||
const chatModel =
|
||||
localStorage.getItem('chatModel') ||
|
||||
(data.chatModelProviders &&
|
||||
data.chatModelProviders[chatModelProvider]?.[0]) ||
|
||||
'';
|
||||
localStorage.getItem("chatModel") ||
|
||||
(data.chatModelProviders && data.chatModelProviders[chatModelProvider]?.[0]) ||
|
||||
"";
|
||||
const embeddingModelProvider =
|
||||
localStorage.getItem('embeddingModelProvider') ||
|
||||
defaultEmbeddingModelProvider ||
|
||||
'';
|
||||
localStorage.getItem("embeddingModelProvider") || defaultEmbeddingModelProvider || "";
|
||||
const embeddingModel =
|
||||
localStorage.getItem('embeddingModel') ||
|
||||
(data.embeddingModelProviders &&
|
||||
data.embeddingModelProviders[embeddingModelProvider]?.[0]) ||
|
||||
'';
|
||||
localStorage.getItem("embeddingModel") ||
|
||||
(data.embeddingModelProviders && data.embeddingModelProviders[embeddingModelProvider]?.[0]) ||
|
||||
"";
|
||||
|
||||
setSelectedChatModelProvider(chatModelProvider);
|
||||
setSelectedChatModel(chatModel);
|
||||
setSelectedEmbeddingModelProvider(embeddingModelProvider);
|
||||
setSelectedEmbeddingModel(embeddingModel);
|
||||
setCustomOpenAIApiKey(localStorage.getItem('openAIApiKey') || '');
|
||||
setCustomOpenAIBaseURL(localStorage.getItem('openAIBaseURL') || '');
|
||||
setCustomOpenAIApiKey(localStorage.getItem("openAIApiKey") || "");
|
||||
setCustomOpenAIBaseURL(localStorage.getItem("openAIBaseURL") || "");
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
|
@ -148,22 +116,19 @@ const SettingsDialog = ({
|
|||
|
||||
try {
|
||||
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
|
||||
localStorage.setItem('chatModelProvider', selectedChatModelProvider!);
|
||||
localStorage.setItem('chatModel', selectedChatModel!);
|
||||
localStorage.setItem(
|
||||
'embeddingModelProvider',
|
||||
selectedEmbeddingModelProvider!,
|
||||
);
|
||||
localStorage.setItem('embeddingModel', selectedEmbeddingModel!);
|
||||
localStorage.setItem('openAIApiKey', customOpenAIApiKey!);
|
||||
localStorage.setItem('openAIBaseURL', customOpenAIBaseURL!);
|
||||
localStorage.setItem("chatModelProvider", selectedChatModelProvider!);
|
||||
localStorage.setItem("chatModel", selectedChatModel!);
|
||||
localStorage.setItem("embeddingModelProvider", selectedEmbeddingModelProvider!);
|
||||
localStorage.setItem("embeddingModel", selectedEmbeddingModel!);
|
||||
localStorage.setItem("openAIApiKey", customOpenAIApiKey!);
|
||||
localStorage.setItem("openAIBaseURL", customOpenAIBaseURL!);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} finally {
|
||||
|
@ -176,11 +141,7 @@ const SettingsDialog = ({
|
|||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-50"
|
||||
onClose={() => setIsOpen(false)}
|
||||
>
|
||||
<Dialog as="div" className="relative z-50" onClose={() => setIsOpen(false)}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
|
@ -204,147 +165,105 @@ const SettingsDialog = ({
|
|||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title className="text-xl font-medium leading-6 dark:text-white">
|
||||
Settings
|
||||
</Dialog.Title>
|
||||
<Dialog.Title className="text-xl font-medium leading-6 dark:text-white">Settings</Dialog.Title>
|
||||
{config && !isLoading && (
|
||||
<div className="flex flex-col space-y-4 mt-6">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Theme
|
||||
</p>
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">Theme</p>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
{config.chatModelProviders && (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Chat model Provider
|
||||
</p>
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">Chat model Provider</p>
|
||||
<Select
|
||||
value={selectedChatModelProvider ?? undefined}
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
setSelectedChatModelProvider(e.target.value);
|
||||
setSelectedChatModel(
|
||||
config.chatModelProviders[e.target.value][0],
|
||||
);
|
||||
setSelectedChatModel(config.chatModelProviders[e.target.value][0]);
|
||||
}}
|
||||
options={Object.keys(config.chatModelProviders).map(
|
||||
(provider) => ({
|
||||
value: provider,
|
||||
label:
|
||||
provider.charAt(0).toUpperCase() +
|
||||
provider.slice(1),
|
||||
}),
|
||||
)}
|
||||
options={Object.keys(config.chatModelProviders).map(provider => ({
|
||||
value: provider,
|
||||
label: provider.charAt(0).toUpperCase() + provider.slice(1),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{selectedChatModelProvider &&
|
||||
selectedChatModelProvider != 'custom_openai' && (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Chat Model
|
||||
</p>
|
||||
<Select
|
||||
value={selectedChatModel ?? undefined}
|
||||
onChange={(e) =>
|
||||
setSelectedChatModel(e.target.value)
|
||||
}
|
||||
options={(() => {
|
||||
const chatModelProvider =
|
||||
config.chatModelProviders[
|
||||
selectedChatModelProvider
|
||||
];
|
||||
{selectedChatModelProvider && selectedChatModelProvider != "custom_openai" && (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">Chat Model</p>
|
||||
<Select
|
||||
value={selectedChatModel ?? undefined}
|
||||
onChange={e => setSelectedChatModel(e.target.value)}
|
||||
options={(() => {
|
||||
const chatModelProvider = config.chatModelProviders[selectedChatModelProvider];
|
||||
|
||||
return chatModelProvider
|
||||
? chatModelProvider.length > 0
|
||||
? chatModelProvider.map((model) => ({
|
||||
value: model,
|
||||
label: model,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
value: '',
|
||||
label: 'No models available',
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
return chatModelProvider
|
||||
? chatModelProvider.length > 0
|
||||
? chatModelProvider.map(model => ({
|
||||
value: model,
|
||||
label: model,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
value: '',
|
||||
label:
|
||||
'Invalid provider, please check backend logs',
|
||||
value: "",
|
||||
label: "No models available",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
})()}
|
||||
]
|
||||
: [
|
||||
{
|
||||
value: "",
|
||||
label: "Invalid provider, please check backend logs",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
})()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{selectedChatModelProvider && selectedChatModelProvider === "custom_openai" && (
|
||||
<>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">Model name</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Model name"
|
||||
defaultValue={selectedChatModel!}
|
||||
onChange={e => setSelectedChatModel(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{selectedChatModelProvider &&
|
||||
selectedChatModelProvider === 'custom_openai' && (
|
||||
<>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Model name
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Model name"
|
||||
defaultValue={selectedChatModel!}
|
||||
onChange={(e) =>
|
||||
setSelectedChatModel(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Custom OpenAI API Key
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Custom OpenAI API Key"
|
||||
defaultValue={customOpenAIApiKey!}
|
||||
onChange={(e) =>
|
||||
setCustomOpenAIApiKey(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Custom OpenAI Base URL
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Custom OpenAI Base URL"
|
||||
defaultValue={customOpenAIBaseURL!}
|
||||
onChange={(e) =>
|
||||
setCustomOpenAIBaseURL(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">Custom OpenAI API Key</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Custom OpenAI API Key"
|
||||
defaultValue={customOpenAIApiKey!}
|
||||
onChange={e => setCustomOpenAIApiKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">Custom OpenAI Base URL</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Custom OpenAI Base URL"
|
||||
defaultValue={customOpenAIBaseURL!}
|
||||
onChange={e => setCustomOpenAIBaseURL(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* Embedding models */}
|
||||
{config.embeddingModelProviders && (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Embedding model Provider
|
||||
</p>
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">Embedding model Provider</p>
|
||||
<Select
|
||||
value={selectedEmbeddingModelProvider ?? undefined}
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
setSelectedEmbeddingModelProvider(e.target.value);
|
||||
setSelectedEmbeddingModel(
|
||||
config.embeddingModelProviders[e.target.value][0],
|
||||
);
|
||||
setSelectedEmbeddingModel(config.embeddingModelProviders[e.target.value][0]);
|
||||
}}
|
||||
options={Object.keys(
|
||||
config.embeddingModelProviders,
|
||||
).map((provider) => ({
|
||||
label:
|
||||
provider.charAt(0).toUpperCase() +
|
||||
provider.slice(1),
|
||||
options={Object.keys(config.embeddingModelProviders).map(provider => ({
|
||||
label: provider.charAt(0).toUpperCase() + provider.slice(1),
|
||||
value: provider,
|
||||
}))}
|
||||
/>
|
||||
|
@ -352,38 +271,31 @@ const SettingsDialog = ({
|
|||
)}
|
||||
{selectedEmbeddingModelProvider && (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Embedding Model
|
||||
</p>
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">Embedding Model</p>
|
||||
<Select
|
||||
value={selectedEmbeddingModel ?? undefined}
|
||||
onChange={(e) =>
|
||||
setSelectedEmbeddingModel(e.target.value)
|
||||
}
|
||||
onChange={e => setSelectedEmbeddingModel(e.target.value)}
|
||||
options={(() => {
|
||||
const embeddingModelProvider =
|
||||
config.embeddingModelProviders[
|
||||
selectedEmbeddingModelProvider
|
||||
];
|
||||
config.embeddingModelProviders[selectedEmbeddingModelProvider];
|
||||
|
||||
return embeddingModelProvider
|
||||
? embeddingModelProvider.length > 0
|
||||
? embeddingModelProvider.map((model) => ({
|
||||
? embeddingModelProvider.map(model => ({
|
||||
label: model,
|
||||
value: model,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
label: 'No embedding models available',
|
||||
value: '',
|
||||
label: "No embedding models available",
|
||||
value: "",
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label:
|
||||
'Invalid provider, please check backend logs',
|
||||
value: '',
|
||||
label: "Invalid provider, please check backend logs",
|
||||
value: "",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
@ -392,14 +304,12 @@ const SettingsDialog = ({
|
|||
</div>
|
||||
)}
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
OpenAI API Key
|
||||
</p>
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">OpenAI API Key</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="OpenAI API Key"
|
||||
defaultValue={config.openaiApiKey}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
setConfig({
|
||||
...config,
|
||||
openaiApiKey: e.target.value,
|
||||
|
@ -408,14 +318,12 @@ const SettingsDialog = ({
|
|||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
Ollama API URL
|
||||
</p>
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">Ollama API URL</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Ollama API URL"
|
||||
defaultValue={config.ollamaApiUrl}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
setConfig({
|
||||
...config,
|
||||
ollamaApiUrl: e.target.value,
|
||||
|
@ -424,14 +332,12 @@ const SettingsDialog = ({
|
|||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||
GROQ API Key
|
||||
</p>
|
||||
<p className="text-black/70 dark:text-white/70 text-sm">GROQ API Key</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="GROQ API Key"
|
||||
defaultValue={config.groqApiKey}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
setConfig({
|
||||
...config,
|
||||
groqApiKey: e.target.value,
|
||||
|
@ -455,11 +361,7 @@ const SettingsDialog = ({
|
|||
className="bg-[#24A0ED] flex flex-row items-center space-x-2 text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full px-4 py-2"
|
||||
disabled={isLoading || isUpdating}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<RefreshCw size={20} className="animate-spin" />
|
||||
) : (
|
||||
<CloudUpload size={20} />
|
||||
)}
|
||||
{isUpdating ? <RefreshCw size={20} className="animate-spin" /> : <CloudUpload size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
'use client';
|
||||
"use client";
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { BookOpenText, Home, Search, SquarePen, Settings } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useSelectedLayoutSegments } from 'next/navigation';
|
||||
import React, { useState, type ReactNode } from 'react';
|
||||
import Layout from './Layout';
|
||||
import SettingsDialog from './SettingsDialog';
|
||||
import { cn } from "@/lib/utils";
|
||||
import { BookOpenText, Home, Search, SquarePen, Settings } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useSelectedLayoutSegments } from "next/navigation";
|
||||
import React, { useState, type ReactNode } from "react";
|
||||
import Layout from "./Layout";
|
||||
import SettingsDialog from "./SettingsDialog";
|
||||
|
||||
const VerticalIconContainer = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-y-3 w-full">{children}</div>
|
||||
);
|
||||
return <div className="flex flex-col items-center gap-y-3 w-full">{children}</div>;
|
||||
};
|
||||
|
||||
const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
||||
|
@ -22,21 +20,21 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
|||
const navLinks = [
|
||||
{
|
||||
icon: Home,
|
||||
href: '/',
|
||||
active: segments.length === 0 || segments.includes('c'),
|
||||
label: 'Home',
|
||||
href: "/",
|
||||
active: segments.length === 0 || segments.includes("c"),
|
||||
label: "Home",
|
||||
},
|
||||
{
|
||||
icon: Search,
|
||||
href: '/discover',
|
||||
active: segments.includes('discover'),
|
||||
label: 'Discover',
|
||||
href: "/discover",
|
||||
active: segments.includes("discover"),
|
||||
label: "Discover",
|
||||
},
|
||||
{
|
||||
icon: BookOpenText,
|
||||
href: '/library',
|
||||
active: segments.includes('library'),
|
||||
label: 'Library',
|
||||
href: "/library",
|
||||
active: segments.includes("library"),
|
||||
label: "Library",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -53,10 +51,8 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
|||
key={i}
|
||||
href={link.href}
|
||||
className={cn(
|
||||
'relative flex flex-row items-center justify-center cursor-pointer hover:bg-black/10 dark:hover:bg-white/10 duration-150 transition w-full py-2 rounded-lg',
|
||||
link.active
|
||||
? 'text-black dark:text-white'
|
||||
: 'text-black/70 dark:text-white/70',
|
||||
"relative flex flex-row items-center justify-center cursor-pointer hover:bg-black/10 dark:hover:bg-white/10 duration-150 transition w-full py-2 rounded-lg",
|
||||
link.active ? "text-black dark:text-white" : "text-black/70 dark:text-white/70",
|
||||
)}
|
||||
>
|
||||
<link.icon />
|
||||
|
@ -67,15 +63,9 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
|||
))}
|
||||
</VerticalIconContainer>
|
||||
|
||||
<Settings
|
||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<Settings onClick={() => setIsSettingsOpen(!isSettingsOpen)} className="cursor-pointer" />
|
||||
|
||||
<SettingsDialog
|
||||
isOpen={isSettingsOpen}
|
||||
setIsOpen={setIsSettingsOpen}
|
||||
/>
|
||||
<SettingsDialog isOpen={isSettingsOpen} setIsOpen={setIsSettingsOpen} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -85,15 +75,11 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
|||
href={link.href}
|
||||
key={i}
|
||||
className={cn(
|
||||
'relative flex flex-col items-center space-y-1 text-center w-full',
|
||||
link.active
|
||||
? 'text-black dark:text-white'
|
||||
: 'text-black dark:text-white/70',
|
||||
"relative flex flex-col items-center space-y-1 text-center w-full",
|
||||
link.active ? "text-black dark:text-white" : "text-black dark:text-white/70",
|
||||
)}
|
||||
>
|
||||
{link.active && (
|
||||
<div className="absolute top-0 -mt-4 h-1 w-full rounded-b-lg bg-black dark:bg-white" />
|
||||
)}
|
||||
{link.active && <div className="absolute top-0 -mt-4 h-1 w-full rounded-b-lg bg-black dark:bg-white" />}
|
||||
<link.icon />
|
||||
<p className="text-xs">{link.label}</p>
|
||||
</Link>
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
'use client';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
"use client";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
|
||||
const ThemeProviderComponent = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const ThemeProviderComponent = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<ThemeProvider attribute="class" enableSystem={false} defaultTheme="dark">
|
||||
{children}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
'use client';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { SunIcon, MoonIcon, MonitorIcon } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Select } from '../SettingsDialog';
|
||||
"use client";
|
||||
import { useTheme } from "next-themes";
|
||||
import { SunIcon, MoonIcon, MonitorIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Select } from "../SettingsDialog";
|
||||
|
||||
type Theme = 'dark' | 'light' | 'system';
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
const ThemeSwitcher = ({ className }: { className?: string }) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
@ -22,20 +22,18 @@ const ThemeSwitcher = ({ className }: { className?: string }) => {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTheme('system')) {
|
||||
const preferDarkScheme = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)',
|
||||
);
|
||||
if (isTheme("system")) {
|
||||
const preferDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
const detectThemeChange = (event: MediaQueryListEvent) => {
|
||||
const theme: Theme = event.matches ? 'dark' : 'light';
|
||||
const theme: Theme = event.matches ? "dark" : "light";
|
||||
setTheme(theme);
|
||||
};
|
||||
|
||||
preferDarkScheme.addEventListener('change', detectThemeChange);
|
||||
preferDarkScheme.addEventListener("change", detectThemeChange);
|
||||
|
||||
return () => {
|
||||
preferDarkScheme.removeEventListener('change', detectThemeChange);
|
||||
preferDarkScheme.removeEventListener("change", detectThemeChange);
|
||||
};
|
||||
}
|
||||
}, [isTheme, setTheme, theme]);
|
||||
|
@ -49,10 +47,10 @@ const ThemeSwitcher = ({ className }: { className?: string }) => {
|
|||
<Select
|
||||
className={className}
|
||||
value={theme}
|
||||
onChange={(e) => handleThemeSwitch(e.target.value as Theme)}
|
||||
onChange={e => handleThemeSwitch(e.target.value as Theme)}
|
||||
options={[
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "dark", label: "Dark" },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue