This commit is contained in:
Jin Yucong 2024-07-05 14:36:50 +08:00
parent 5b1aaee605
commit 3b737a078a
63 changed files with 1132 additions and 1853 deletions

View file

@ -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>
)}

View file

@ -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>
)

View file

@ -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) {

View file

@ -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>
);

View file

@ -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"

View file

@ -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 (

View file

@ -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)}

View file

@ -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>
)}

View file

@ -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"

View file

@ -1,4 +1,4 @@
import { CopyPlus } from 'lucide-react';
import { CopyPlus } from "lucide-react";
const Attach = () => {
return (

View file

@ -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

View file

@ -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>

View file

@ -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">

View file

@ -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>
);

View file

@ -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>

View file

@ -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}

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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" },
]}
/>
);