commit
21b315d14b
|
@ -4,6 +4,7 @@ import './globals.css';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import Sidebar from '@/components/Sidebar';
|
import Sidebar from '@/components/Sidebar';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
|
import ThemeProvider from '@/components/theme/Provider';
|
||||||
|
|
||||||
const montserrat = Montserrat({
|
const montserrat = Montserrat({
|
||||||
weight: ['300', '400', '500', '700'],
|
weight: ['300', '400', '500', '700'],
|
||||||
|
@ -24,18 +25,20 @@ export default function RootLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html className="h-full" lang="en">
|
<html className="h-full" lang="en" suppressHydrationWarning>
|
||||||
<body className={cn('h-full', montserrat.className)}>
|
<body className={cn('h-full', montserrat.className)}>
|
||||||
<Sidebar>{children}</Sidebar>
|
<ThemeProvider>
|
||||||
<Toaster
|
<Sidebar>{children}</Sidebar>
|
||||||
toastOptions={{
|
<Toaster
|
||||||
unstyled: true,
|
toastOptions={{
|
||||||
classNames: {
|
unstyled: true,
|
||||||
toast:
|
classNames: {
|
||||||
'bg-[#111111] text-white rounded-lg p-4 flex flex-row items-center space-x-2',
|
toast:
|
||||||
},
|
'bg-light-primary dark:bg-dark-primary text-white rounded-lg p-4 flex flex-row items-center space-x-2',
|
||||||
}}
|
},
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
@ -66,7 +66,7 @@ const Chat = ({
|
||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
/>
|
/>
|
||||||
{!isLast && msg.role === 'assistant' && (
|
{!isLast && msg.role === 'assistant' && (
|
||||||
<div className="h-px w-full bg-[#1C1C1C]" />
|
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -323,7 +323,7 @@ const ChatWindow = () => {
|
||||||
<div className="flex flex-row items-center justify-center min-h-screen">
|
<div className="flex flex-row items-center justify-center min-h-screen">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="w-8 h-8 text-[#202020] animate-spin fill-[#ffffff3b]"
|
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
|
||||||
viewBox="0 0 100 101"
|
viewBox="0 0 100 101"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import EmptyChatMessageInput from './EmptyChatMessageInput';
|
import EmptyChatMessageInput from './EmptyChatMessageInput';
|
||||||
|
import ThemeSwitcher from './theme/Switcher';
|
||||||
|
|
||||||
const EmptyChat = ({
|
const EmptyChat = ({
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
@ -10,15 +11,19 @@ const EmptyChat = ({
|
||||||
setFocusMode: (mode: string) => void;
|
setFocusMode: (mode: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-8">
|
<div className="relative">
|
||||||
<h2 className="text-white/70 text-3xl font-medium -mt-8">
|
<ThemeSwitcher size={17} className="absolute top-2 right-0 lg:hidden" />
|
||||||
Research begins here.
|
|
||||||
</h2>
|
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-8">
|
||||||
<EmptyChatMessageInput
|
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
|
||||||
sendMessage={sendMessage}
|
Research begins here.
|
||||||
focusMode={focusMode}
|
</h2>
|
||||||
setFocusMode={setFocusMode}
|
<EmptyChatMessageInput
|
||||||
/>
|
sendMessage={sendMessage}
|
||||||
|
focusMode={focusMode}
|
||||||
|
setFocusMode={setFocusMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { ArrowRight } from 'lucide-react';
|
import { ArrowRight } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { CopilotToggle, Focus } from './MessageInputActions';
|
import CopilotToggle from './MessageInputActions/Copilot';
|
||||||
|
import Focus from './MessageInputActions/Focus';
|
||||||
|
|
||||||
const EmptyChatMessageInput = ({
|
const EmptyChatMessageInput = ({
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
@ -31,12 +32,12 @@ const EmptyChatMessageInput = ({
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col bg-[#111111] px-5 pt-5 pb-2 rounded-lg w-full border border-[#1C1C1C]">
|
<div className="flex flex-col bg-light-secondary dark:bg-dark-secondary px-5 pt-5 pb-2 rounded-lg w-full border border-light-200 dark:border-dark-200">
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
value={message}
|
value={message}
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
minRows={2}
|
minRows={2}
|
||||||
className="bg-transparent placeholder:text-white/50 text-sm text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
|
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..."
|
placeholder="Ask anything..."
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-row items-center justify-between mt-4">
|
<div className="flex flex-row items-center justify-between mt-4">
|
||||||
|
@ -51,7 +52,7 @@ const EmptyChatMessageInput = ({
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
disabled={message.trim().length === 0}
|
disabled={message.trim().length === 0}
|
||||||
className="bg-[#24A0ED] text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full p-2"
|
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2"
|
||||||
>
|
>
|
||||||
<ArrowRight className="bg-background" size={17} />
|
<ArrowRight className="bg-background" size={17} />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<main className="lg:pl-20 bg-[#0A0A0A] min-h-screen">
|
<main className="lg:pl-20 bg-light-primary dark:bg-dark-primary min-h-screen">
|
||||||
<div className="max-w-screen-lg lg:mx-auto mx-4">{children}</div>
|
<div className="max-w-screen-lg lg:mx-auto mx-4">{children}</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,7 +19,7 @@ const Copy = ({
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 1000);
|
setTimeout(() => setCopied(false), 1000);
|
||||||
}}
|
}}
|
||||||
className="p-2 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white"
|
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"
|
||||||
>
|
>
|
||||||
{copied ? <Check size={18} /> : <ClipboardList size={18} />}
|
{copied ? <Check size={18} /> : <ClipboardList size={18} />}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -10,7 +10,7 @@ const Rewrite = ({
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => rewrite(messageId)}
|
onClick={() => rewrite(messageId)}
|
||||||
className="py-2 px-3 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white flex flex-row items-center space-x-1"
|
className="py-2 px-3 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 flex flex-row items-center space-x-1"
|
||||||
>
|
>
|
||||||
<ArrowLeftRight size={18} />
|
<ArrowLeftRight size={18} />
|
||||||
<p className="text-xs font-medium">Rewrite</p>
|
<p className="text-xs font-medium">Rewrite</p>
|
||||||
|
|
|
@ -55,7 +55,7 @@ const MessageBox = ({
|
||||||
message.content.replace(
|
message.content.replace(
|
||||||
regex,
|
regex,
|
||||||
(_, number) =>
|
(_, number) =>
|
||||||
`<a href="${message.sources?.[number - 1]?.metadata?.url}" target="_blank" className="bg-[#1C1C1C] px-1 rounded ml-1 no-underline text-xs text-white/70 relative">${number}</a>`,
|
`<a href="${message.sources?.[number - 1]?.metadata?.url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${number}</a>`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ const MessageBox = ({
|
||||||
<div>
|
<div>
|
||||||
{message.role === 'user' && (
|
{message.role === 'user' && (
|
||||||
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8')}>
|
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8')}>
|
||||||
<h2 className="text-white font-medium text-3xl lg:w-9/12">
|
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
|
||||||
{message.content}
|
{message.content}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -85,8 +85,10 @@ const MessageBox = ({
|
||||||
{message.sources && message.sources.length > 0 && (
|
{message.sources && message.sources.length > 0 && (
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<BookCopy className="text-white" size={20} />
|
<BookCopy className="text-black dark:text-white" size={20} />
|
||||||
<h3 className="text-white font-medium text-xl">Sources</h3>
|
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||||
|
Sources
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<MessageSources sources={message.sources} />
|
<MessageSources sources={message.sources} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -95,20 +97,27 @@ const MessageBox = ({
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<Disc3
|
<Disc3
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-white',
|
'text-black dark:text-white',
|
||||||
isLast && loading ? 'animate-spin' : 'animate-none',
|
isLast && loading ? 'animate-spin' : 'animate-none',
|
||||||
)}
|
)}
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
<h3 className="text-white font-medium text-xl">Answer</h3>
|
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||||
|
Answer
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<Markdown className="prose max-w-none break-words prose-invert prose-p:leading-relaxed prose-pre:p-0 text-white text-sm md:text-base font-medium">
|
<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',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{parsedMessage}
|
{parsedMessage}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
{loading && isLast ? null : (
|
{loading && isLast ? null : (
|
||||||
<div className="flex flex-row items-center justify-between w-full text-white py-4 -mx-2">
|
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white py-4 -mx-2">
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center space-x-1">
|
||||||
{/* <button className="p-2 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white">
|
{/* <button 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 text-black dark:hover:text-white">
|
||||||
<Share size={18} />
|
<Share size={18} />
|
||||||
</button> */}
|
</button> */}
|
||||||
<Rewrite rewrite={rewrite} messageId={message.id} />
|
<Rewrite rewrite={rewrite} messageId={message.id} />
|
||||||
|
@ -123,7 +132,7 @@ const MessageBox = ({
|
||||||
start();
|
start();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="p-2 text-white/70 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white"
|
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' ? (
|
{speechStatus === 'started' ? (
|
||||||
<StopCircle size={18} />
|
<StopCircle size={18} />
|
||||||
|
@ -140,8 +149,8 @@ const MessageBox = ({
|
||||||
message.role === 'assistant' &&
|
message.role === 'assistant' &&
|
||||||
!loading && (
|
!loading && (
|
||||||
<>
|
<>
|
||||||
<div className="h-px w-full bg-[#1C1C1C]" />
|
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||||
<div className="flex flex-col space-y-3 text-white">
|
<div className="flex flex-col space-y-3 text-black dark:text-white">
|
||||||
<div className="flex flex-row items-center space-x-2 mt-4">
|
<div className="flex flex-row items-center space-x-2 mt-4">
|
||||||
<Layers3 />
|
<Layers3 />
|
||||||
<h3 className="text-xl font-medium">Related</h3>
|
<h3 className="text-xl font-medium">Related</h3>
|
||||||
|
@ -152,7 +161,7 @@ const MessageBox = ({
|
||||||
className="flex flex-col space-y-3 text-sm"
|
className="flex flex-col space-y-3 text-sm"
|
||||||
key={i}
|
key={i}
|
||||||
>
|
>
|
||||||
<div className="h-px w-full bg-[#1C1C1C]" />
|
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sendMessage(suggestion);
|
sendMessage(suggestion);
|
||||||
|
@ -162,7 +171,10 @@ const MessageBox = ({
|
||||||
<p className="transition duration-200 hover:text-[#24A0ED]">
|
<p className="transition duration-200 hover:text-[#24A0ED]">
|
||||||
{suggestion}
|
{suggestion}
|
||||||
</p>
|
</p>
|
||||||
<Plus size={20} className="text-[#24A0ED]" />
|
<Plus
|
||||||
|
size={20}
|
||||||
|
className="text-[#24A0ED] flex-shrink-0"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
const MessageBoxLoading = () => {
|
const MessageBoxLoading = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-2 w-full lg:w-9/12 bg-[#111111] animate-pulse rounded-lg p-3">
|
<div className="flex flex-col space-y-2 w-full lg:w-9/12 bg-light-primary dark:bg-dark-primary animate-pulse rounded-lg p-3">
|
||||||
<div className="h-2 rounded-full w-full bg-[#1c1c1c]" />
|
<div className="h-2 rounded-full w-full bg-light-secondary dark:bg-dark-secondary" />
|
||||||
<div className="h-2 rounded-full w-9/12 bg-[#1c1c1c]" />
|
<div className="h-2 rounded-full w-9/12 bg-light-secondary dark:bg-dark-secondary" />
|
||||||
<div className="h-2 rounded-full w-10/12 bg-[#1c1c1c]" />
|
<div className="h-2 rounded-full w-10/12 bg-light-secondary dark:bg-dark-secondary" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { cn } from '@/lib/utils';
|
||||||
import { ArrowUp } from 'lucide-react';
|
import { ArrowUp } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { Attach, CopilotToggle } from './MessageInputActions';
|
import Attach from './MessageInputActions/Attach';
|
||||||
|
import CopilotToggle from './MessageInputActions/Copilot';
|
||||||
|
|
||||||
const MessageInput = ({
|
const MessageInput = ({
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
@ -40,7 +41,7 @@ const MessageInput = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-[#111111] p-4 flex items-center overflow-hidden border border-[#1C1C1C]',
|
'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 === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -51,7 +52,7 @@ const MessageInput = ({
|
||||||
onHeightChange={(height, props) => {
|
onHeightChange={(height, props) => {
|
||||||
setTextareaRows(Math.ceil(height / props.rowHeight));
|
setTextareaRows(Math.ceil(height / props.rowHeight));
|
||||||
}}
|
}}
|
||||||
className="transition bg-transparent placeholder:text-white/50 placeholder:text-sm text-sm 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"
|
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"
|
placeholder="Ask a follow-up"
|
||||||
/>
|
/>
|
||||||
{mode === 'single' && (
|
{mode === 'single' && (
|
||||||
|
@ -62,7 +63,7 @@ const MessageInput = ({
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
disabled={message.trim().length === 0 || loading}
|
disabled={message.trim().length === 0 || loading}
|
||||||
className="bg-[#24A0ED] text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full p-2"
|
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
|
||||||
>
|
>
|
||||||
<ArrowUp className="bg-background" size={17} />
|
<ArrowUp className="bg-background" size={17} />
|
||||||
</button>
|
</button>
|
||||||
|
@ -78,7 +79,7 @@ const MessageInput = ({
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
disabled={message.trim().length === 0 || loading}
|
disabled={message.trim().length === 0 || loading}
|
||||||
className="bg-[#24A0ED] text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full p-2"
|
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"
|
||||||
>
|
>
|
||||||
<ArrowUp className="bg-background" size={17} />
|
<ArrowUp className="bg-background" size={17} />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { CopyPlus } from 'lucide-react';
|
||||||
|
|
||||||
|
const Attach = () => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<CopyPlus />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Attach;
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Switch } from '@headlessui/react';
|
||||||
|
|
||||||
|
const CopilotToggle = ({
|
||||||
|
copilotEnabled,
|
||||||
|
setCopilotEnabled,
|
||||||
|
}: {
|
||||||
|
copilotEnabled: boolean;
|
||||||
|
setCopilotEnabled: (enabled: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer">
|
||||||
|
<Switch
|
||||||
|
checked={copilotEnabled}
|
||||||
|
onChange={setCopilotEnabled}
|
||||||
|
className="bg-light-secondary dark:bg-dark-secondary border border-light-200/70 dark:border-dark-200 relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
|
||||||
|
>
|
||||||
|
<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',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
<p
|
||||||
|
onClick={() => setCopilotEnabled(!copilotEnabled)}
|
||||||
|
className={cn(
|
||||||
|
'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',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Copilot
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CopilotToggle;
|
|
@ -1,28 +1,16 @@
|
||||||
import {
|
import {
|
||||||
BadgePercent,
|
BadgePercent,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
CopyPlus,
|
|
||||||
Globe,
|
Globe,
|
||||||
Pencil,
|
Pencil,
|
||||||
ScanEye,
|
ScanEye,
|
||||||
SwatchBook,
|
SwatchBook,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Popover, Switch, Transition } from '@headlessui/react';
|
import { Popover, Transition } from '@headlessui/react';
|
||||||
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
|
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
|
||||||
export const Attach = () => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="p-2 text-white/50 rounded-xl hover:bg-[#1c1c1c] transition duration-200 hover:text-white"
|
|
||||||
>
|
|
||||||
<CopyPlus />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const focusModes = [
|
const focusModes = [
|
||||||
{
|
{
|
||||||
key: 'webSearch',
|
key: 'webSearch',
|
||||||
|
@ -74,7 +62,7 @@ const focusModes = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Focus = ({
|
const Focus = ({
|
||||||
focusMode,
|
focusMode,
|
||||||
setFocusMode,
|
setFocusMode,
|
||||||
}: {
|
}: {
|
||||||
|
@ -85,7 +73,7 @@ export const Focus = ({
|
||||||
<Popover className="fixed w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
<Popover className="fixed w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-2 text-white/50 rounded-xl hover:bg-[#1c1c1c] active:scale-95 transition duration-200 hover:text-white"
|
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">
|
<div className="flex flex-row items-center space-x-1">
|
||||||
|
@ -109,7 +97,7 @@ export const Focus = ({
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute z-10 w-full">
|
<Popover.Panel className="absolute z-10 w-full">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-1 bg-[#0A0A0A] border rounded-lg border-[#1c1c1c] w-full p-2 max-h-[200px] md:max-h-none overflow-y-auto">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-1 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-2 max-h-[200px] md:max-h-none overflow-y-auto">
|
||||||
{focusModes.map((mode, i) => (
|
{focusModes.map((mode, i) => (
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
onClick={() => setFocusMode(mode.key)}
|
onClick={() => setFocusMode(mode.key)}
|
||||||
|
@ -117,20 +105,24 @@ export const Focus = ({
|
||||||
className={cn(
|
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
|
focusMode === mode.key
|
||||||
? 'bg-[#111111]'
|
? 'bg-light-secondary dark:bg-dark-secondary'
|
||||||
: 'hover:bg-[#111111]',
|
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-row items-center space-x-1',
|
'flex flex-row items-center space-x-1',
|
||||||
focusMode === mode.key ? 'text-[#24A0ED]' : 'text-white',
|
focusMode === mode.key
|
||||||
|
? 'text-[#24A0ED]'
|
||||||
|
: 'text-black dark:text-white',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{mode.icon}
|
{mode.icon}
|
||||||
<p className="text-sm font-medium">{mode.title}</p>
|
<p className="text-sm font-medium">{mode.title}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-white/70 text-xs">{mode.description}</p>
|
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||||
|
{mode.description}
|
||||||
|
</p>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -140,41 +132,4 @@ export const Focus = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CopilotToggle = ({
|
export default Focus;
|
||||||
copilotEnabled,
|
|
||||||
setCopilotEnabled,
|
|
||||||
}: {
|
|
||||||
copilotEnabled: boolean;
|
|
||||||
setCopilotEnabled: (enabled: boolean) => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer">
|
|
||||||
<Switch
|
|
||||||
checked={copilotEnabled}
|
|
||||||
onChange={setCopilotEnabled}
|
|
||||||
className="bg-[#111111] border border-[#1C1C1C] relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Copilot</span>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
copilotEnabled
|
|
||||||
? 'translate-x-6 bg-[#24A0ED]'
|
|
||||||
: 'translate-x-1 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',
|
|
||||||
copilotEnabled
|
|
||||||
? 'text-[#24A0ED]'
|
|
||||||
: 'text-white/50 group-hover:text-white',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Copilot
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -20,12 +20,12 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2">
|
||||||
{sources.slice(0, 3).map((source, i) => (
|
{sources.slice(0, 3).map((source, i) => (
|
||||||
<a
|
<a
|
||||||
className="bg-[#111111] hover:bg-[#1c1c1c] transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
|
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
|
||||||
key={i}
|
key={i}
|
||||||
href={source.metadata.url}
|
href={source.metadata.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<p className="text-white text-xs overflow-hidden whitespace-nowrap text-ellipsis">
|
<p className="dark:text-white text-xs overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
{source.metadata.title}
|
{source.metadata.title}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-row items-center justify-between">
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
@ -37,12 +37,12 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
||||||
alt="favicon"
|
alt="favicon"
|
||||||
className="rounded-lg h-4 w-4"
|
className="rounded-lg h-4 w-4"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center space-x-1 text-white/50 text-xs">
|
<div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs">
|
||||||
<div className="bg-white/50 h-[4px] w-[4px] rounded-full" />
|
<div className="bg-black/50 dark:bg-white/50 h-[4px] w-[4px] rounded-full" />
|
||||||
<span>{i + 1}</span>
|
<span>{i + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,7 +51,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
||||||
{sources.length > 3 && (
|
{sources.length > 3 && (
|
||||||
<button
|
<button
|
||||||
onClick={openModal}
|
onClick={openModal}
|
||||||
className="bg-[#111111] hover:bg-[#1c1c1c] transition duration-200 rounded-lg px-4 py-2 flex flex-col justify-between space-y-2"
|
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center space-x-1">
|
||||||
{sources.slice(3, 6).map((source, i) => (
|
{sources.slice(3, 6).map((source, i) => (
|
||||||
|
@ -65,7 +65,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-white/50">
|
<p className="text-xs text-black/50 dark:text-white/50">
|
||||||
View {sources.length - 3} more
|
View {sources.length - 3} more
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
|
@ -83,19 +83,19 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
||||||
leaveFrom="opacity-100 scale-200"
|
leaveFrom="opacity-100 scale-200"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-md transform rounded-2xl bg-[#111111] border border-[#1c1c1c] p-6 text-left align-middle shadow-xl transition-all">
|
<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 text-white">
|
<Dialog.Title className="text-lg font-medium leading-6 dark:text-white">
|
||||||
Sources
|
Sources
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="grid grid-cols-2 gap-2 overflow-auto max-h-[300px] mt-2 pr-2">
|
<div className="grid grid-cols-2 gap-2 overflow-auto max-h-[300px] mt-2 pr-2">
|
||||||
{sources.map((source, i) => (
|
{sources.map((source, i) => (
|
||||||
<a
|
<a
|
||||||
className="bg-[#111111] hover:bg-[#1c1c1c] border border-[#1c1c1c] transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
|
className="bg-light-secondary hover:bg-light-200 dark:bg-dark-secondary dark:hover:bg-dark-200 border border-light-200 dark:border-dark-200 transition duration-200 rounded-lg p-3 flex flex-col space-y-2 font-medium"
|
||||||
key={i}
|
key={i}
|
||||||
href={source.metadata.url}
|
href={source.metadata.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<p className="text-white text-xs overflow-hidden whitespace-nowrap text-ellipsis">
|
<p className="dark:text-white text-xs overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
{source.metadata.title}
|
{source.metadata.title}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-row items-center justify-between">
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
@ -107,15 +107,15 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
||||||
alt="favicon"
|
alt="favicon"
|
||||||
className="rounded-lg h-4 w-4"
|
className="rounded-lg h-4 w-4"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
|
<p className="text-xs text-black/50 dark:text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
{source.metadata.url.replace(
|
{source.metadata.url.replace(
|
||||||
/.+\/\/|www.|\..+/g,
|
/.+\/\/|www.|\..+/g,
|
||||||
'',
|
'',
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center space-x-1 text-white/50 text-xs">
|
<div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs">
|
||||||
<div className="bg-white/50 h-[4px] w-[4px] rounded-full" />
|
<div className="bg-black/50 dark:bg-white/50 h-[4px] w-[4px] rounded-full" />
|
||||||
<span>{i + 1}</span>
|
<span>{i + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Clock, Edit, Share, Trash } from 'lucide-react';
|
||||||
import { Message } from './ChatWindow';
|
import { Message } from './ChatWindow';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { formatTimeDifference } from '@/lib/utils';
|
import { formatTimeDifference } from '@/lib/utils';
|
||||||
|
import ThemeSwitcher from './theme/Switcher';
|
||||||
|
|
||||||
const Navbar = ({ messages }: { messages: Message[] }) => {
|
const Navbar = ({ messages }: { messages: Message[] }) => {
|
||||||
const [title, setTitle] = useState<string>('');
|
const [title, setTitle] = useState<string>('');
|
||||||
|
@ -38,7 +39,7 @@ const Navbar = ({ messages }: { messages: Message[] }) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
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-white/70 border-b bg-[#0A0A0A] border-[#1C1C1C]">
|
<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
|
<Edit
|
||||||
size={17}
|
size={17}
|
||||||
className="active:scale-95 transition duration-100 cursor-pointer lg:hidden"
|
className="active:scale-95 transition duration-100 cursor-pointer lg:hidden"
|
||||||
|
@ -48,6 +49,9 @@ const Navbar = ({ messages }: { messages: Message[] }) => {
|
||||||
<p className="text-xs">{timeAgo} ago</p>
|
<p className="text-xs">{timeAgo} ago</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="hidden lg:flex">{title}</p>
|
<p className="hidden lg:flex">{title}</p>
|
||||||
|
|
||||||
|
<ThemeSwitcher size={17} className="lg:hidden ml-auto mr-4" />
|
||||||
|
|
||||||
<div className="flex flex-row items-center space-x-4">
|
<div className="flex flex-row items-center space-x-4">
|
||||||
<Share
|
<Share
|
||||||
size={17}
|
size={17}
|
||||||
|
|
|
@ -62,7 +62,7 @@ const SearchImages = ({
|
||||||
);
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}}
|
}}
|
||||||
className="border border-dashed border-[#1C1C1C] hover:bg-[#1c1c1c] active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg text-white text-sm w-full"
|
className="border border-dashed border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg dark:text-white text-sm w-full"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<ImagesIcon size={17} />
|
<ImagesIcon size={17} />
|
||||||
|
@ -76,7 +76,7 @@ const SearchImages = ({
|
||||||
{[...Array(4)].map((_, i) => (
|
{[...Array(4)].map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="bg-[#1C1C1C] h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
|
className="bg-light-secondary dark:bg-dark-secondary h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -120,7 +120,7 @@ const SearchImages = ({
|
||||||
{images.length > 4 && (
|
{images.length > 4 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
className="bg-[#111111] hover:bg-[#1c1c1c] transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
|
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center space-x-1">
|
||||||
{images.slice(3, 6).map((image, i) => (
|
{images.slice(3, 6).map((image, i) => (
|
||||||
|
@ -132,7 +132,7 @@ const SearchImages = ({
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-white/70 text-xs">
|
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||||
View {images.length - 3} more
|
View {images.length - 3} more
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -77,7 +77,7 @@ const Searchvideos = ({
|
||||||
);
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}}
|
}}
|
||||||
className="border border-dashed border-[#1C1C1C] hover:bg-[#1c1c1c] active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg text-white text-sm w-full"
|
className="border border-dashed border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg dark:text-white text-sm w-full"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<VideoIcon size={17} />
|
<VideoIcon size={17} />
|
||||||
|
@ -91,7 +91,7 @@ const Searchvideos = ({
|
||||||
{[...Array(4)].map((_, i) => (
|
{[...Array(4)].map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="bg-[#1C1C1C] h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
|
className="bg-light-secondary dark:bg-dark-secondary h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,7 +118,7 @@ const Searchvideos = ({
|
||||||
alt={video.title}
|
alt={video.title}
|
||||||
className="relative h-full w-full aspect-video object-cover rounded-lg"
|
className="relative h-full w-full aspect-video object-cover rounded-lg"
|
||||||
/>
|
/>
|
||||||
<div className="absolute bg-black/70 text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
|
<div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
|
||||||
<PlayCircle size={15} />
|
<PlayCircle size={15} />
|
||||||
<p className="text-xs">Video</p>
|
<p className="text-xs">Video</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -142,7 +142,7 @@ const Searchvideos = ({
|
||||||
alt={video.title}
|
alt={video.title}
|
||||||
className="relative h-full w-full aspect-video object-cover rounded-lg"
|
className="relative h-full w-full aspect-video object-cover rounded-lg"
|
||||||
/>
|
/>
|
||||||
<div className="absolute bg-black/70 text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
|
<div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
|
||||||
<PlayCircle size={15} />
|
<PlayCircle size={15} />
|
||||||
<p className="text-xs">Video</p>
|
<p className="text-xs">Video</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -151,7 +151,7 @@ const Searchvideos = ({
|
||||||
{videos.length > 4 && (
|
{videos.length > 4 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
className="bg-[#111111] hover:bg-[#1c1c1c] transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
|
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center space-x-1">
|
||||||
{videos.slice(3, 6).map((video, i) => (
|
{videos.slice(3, 6).map((video, i) => (
|
||||||
|
@ -163,7 +163,7 @@ const Searchvideos = ({
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-white/70 text-xs">
|
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||||
View {videos.length - 3} more
|
View {videos.length - 3} more
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,6 +1,52 @@
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { Dialog, Transition } from '@headlessui/react';
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react';
|
import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react';
|
||||||
import React, { Fragment, useEffect, useState } from 'react';
|
import React, {
|
||||||
|
Fragment,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
type SelectHTMLAttributes,
|
||||||
|
} from 'react';
|
||||||
|
import ThemeSwitcher from './theme/Switcher';
|
||||||
|
|
||||||
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = ({ className, ...restProps }: InputProps) => {
|
||||||
|
return (
|
||||||
|
<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',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||||
|
options: { value: string; label: string; disabled?: boolean }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Select = ({ className, options, ...restProps }: SelectProps) => {
|
||||||
|
return (
|
||||||
|
<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',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{options.map(({ label, value, disabled }) => {
|
||||||
|
return (
|
||||||
|
<option key={value} value={value} disabled={disabled}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface SettingsType {
|
interface SettingsType {
|
||||||
chatModelProviders: {
|
chatModelProviders: {
|
||||||
|
@ -145,7 +191,7 @@ const SettingsDialog = ({
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-black/50" />
|
<div className="fixed inset-0 bg-white/50 dark:bg-black/50" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
|
@ -158,18 +204,24 @@ const SettingsDialog = ({
|
||||||
leaveFrom="opacity-100 scale-200"
|
leaveFrom="opacity-100 scale-200"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-md transform rounded-2xl bg-[#111111] border border-[#1c1c1c] p-6 text-left align-middle shadow-xl transition-all">
|
<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 text-white">
|
<Dialog.Title className="text-xl font-medium leading-6 dark:text-white">
|
||||||
Settings
|
Settings
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
{config && !isLoading && (
|
{config && !isLoading && (
|
||||||
<div className="flex flex-col space-y-4 mt-6">
|
<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>
|
||||||
|
<ThemeSwitcher />
|
||||||
|
</div>
|
||||||
{config.chatModelProviders && (
|
{config.chatModelProviders && (
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Chat model Provider
|
Chat model Provider
|
||||||
</p>
|
</p>
|
||||||
<select
|
<Select
|
||||||
value={selectedChatModelProvider ?? undefined}
|
value={selectedChatModelProvider ?? undefined}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedChatModelProvider(e.target.value);
|
setSelectedChatModelProvider(e.target.value);
|
||||||
|
@ -177,97 +229,99 @@ const SettingsDialog = ({
|
||||||
config.chatModelProviders[e.target.value][0],
|
config.chatModelProviders[e.target.value][0],
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
|
options={Object.keys(config.chatModelProviders).map(
|
||||||
>
|
(provider) => ({
|
||||||
{Object.keys(config.chatModelProviders).map(
|
value: provider,
|
||||||
(provider) => (
|
label:
|
||||||
<option key={provider} value={provider}>
|
provider.charAt(0).toUpperCase() +
|
||||||
{provider.charAt(0).toUpperCase() +
|
provider.slice(1),
|
||||||
provider.slice(1)}
|
}),
|
||||||
</option>
|
|
||||||
),
|
|
||||||
)}
|
)}
|
||||||
</select>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedChatModelProvider &&
|
{selectedChatModelProvider &&
|
||||||
selectedChatModelProvider != 'custom_openai' && (
|
selectedChatModelProvider != 'custom_openai' && (
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-white/70 text-sm">Chat Model</p>
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
<select
|
Chat Model
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
value={selectedChatModel ?? undefined}
|
value={selectedChatModel ?? undefined}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setSelectedChatModel(e.target.value)
|
setSelectedChatModel(e.target.value)
|
||||||
}
|
}
|
||||||
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
|
options={(() => {
|
||||||
>
|
const chatModelProvider =
|
||||||
{config.chatModelProviders[
|
|
||||||
selectedChatModelProvider
|
|
||||||
] ? (
|
|
||||||
config.chatModelProviders[
|
|
||||||
selectedChatModelProvider
|
|
||||||
].length > 0 ? (
|
|
||||||
config.chatModelProviders[
|
config.chatModelProviders[
|
||||||
selectedChatModelProvider
|
selectedChatModelProvider
|
||||||
].map((model) => (
|
];
|
||||||
<option key={model} value={model}>
|
|
||||||
{model}
|
return chatModelProvider
|
||||||
</option>
|
? chatModelProvider.length > 0
|
||||||
))
|
? chatModelProvider.map((model) => ({
|
||||||
) : (
|
value: model,
|
||||||
<option value="" disabled>
|
label: model,
|
||||||
No models available
|
}))
|
||||||
</option>
|
: [
|
||||||
)
|
{
|
||||||
) : (
|
value: '',
|
||||||
<option value="" disabled>
|
label: 'No models available',
|
||||||
Invalid provider, please check backend logs
|
disabled: true,
|
||||||
</option>
|
},
|
||||||
)}
|
]
|
||||||
</select>
|
: [
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
label:
|
||||||
|
'Invalid provider, please check backend logs',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
})()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedChatModelProvider &&
|
{selectedChatModelProvider &&
|
||||||
selectedChatModelProvider === 'custom_openai' && (
|
selectedChatModelProvider === 'custom_openai' && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-white/70 text-sm">Model name</p>
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
<input
|
Model name
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Model name"
|
placeholder="Model name"
|
||||||
defaultValue={selectedChatModel!}
|
defaultValue={selectedChatModel!}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setSelectedChatModel(e.target.value)
|
setSelectedChatModel(e.target.value)
|
||||||
}
|
}
|
||||||
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Custom OpenAI API Key
|
Custom OpenAI API Key
|
||||||
</p>
|
</p>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Custom OpenAI API Key"
|
placeholder="Custom OpenAI API Key"
|
||||||
defaultValue={customOpenAIApiKey!}
|
defaultValue={customOpenAIApiKey!}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setCustomOpenAIApiKey(e.target.value)
|
setCustomOpenAIApiKey(e.target.value)
|
||||||
}
|
}
|
||||||
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Custom OpenAI Base URL
|
Custom OpenAI Base URL
|
||||||
</p>
|
</p>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Custom OpenAI Base URL"
|
placeholder="Custom OpenAI Base URL"
|
||||||
defaultValue={customOpenAIBaseURL!}
|
defaultValue={customOpenAIBaseURL!}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setCustomOpenAIBaseURL(e.target.value)
|
setCustomOpenAIBaseURL(e.target.value)
|
||||||
}
|
}
|
||||||
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -275,10 +329,10 @@ const SettingsDialog = ({
|
||||||
{/* Embedding models */}
|
{/* Embedding models */}
|
||||||
{config.embeddingModelProviders && (
|
{config.embeddingModelProviders && (
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Embedding model Provider
|
Embedding model Provider
|
||||||
</p>
|
</p>
|
||||||
<select
|
<Select
|
||||||
value={selectedEmbeddingModelProvider ?? undefined}
|
value={selectedEmbeddingModelProvider ?? undefined}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedEmbeddingModelProvider(e.target.value);
|
setSelectedEmbeddingModelProvider(e.target.value);
|
||||||
|
@ -286,58 +340,63 @@ const SettingsDialog = ({
|
||||||
config.embeddingModelProviders[e.target.value][0],
|
config.embeddingModelProviders[e.target.value][0],
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
|
options={Object.keys(
|
||||||
>
|
config.embeddingModelProviders,
|
||||||
{Object.keys(config.embeddingModelProviders).map(
|
).map((provider) => ({
|
||||||
(provider) => (
|
label:
|
||||||
<option key={provider} value={provider}>
|
provider.charAt(0).toUpperCase() +
|
||||||
{provider.charAt(0).toUpperCase() +
|
provider.slice(1),
|
||||||
provider.slice(1)}
|
value: provider,
|
||||||
</option>
|
}))}
|
||||||
),
|
/>
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedEmbeddingModelProvider && (
|
{selectedEmbeddingModelProvider && (
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-white/70 text-sm">Embedding Model</p>
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
<select
|
Embedding Model
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
value={selectedEmbeddingModel ?? undefined}
|
value={selectedEmbeddingModel ?? undefined}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setSelectedEmbeddingModel(e.target.value)
|
setSelectedEmbeddingModel(e.target.value)
|
||||||
}
|
}
|
||||||
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
|
options={(() => {
|
||||||
>
|
const embeddingModelProvider =
|
||||||
{config.embeddingModelProviders[
|
|
||||||
selectedEmbeddingModelProvider
|
|
||||||
] ? (
|
|
||||||
config.embeddingModelProviders[
|
|
||||||
selectedEmbeddingModelProvider
|
|
||||||
].length > 0 ? (
|
|
||||||
config.embeddingModelProviders[
|
config.embeddingModelProviders[
|
||||||
selectedEmbeddingModelProvider
|
selectedEmbeddingModelProvider
|
||||||
].map((model) => (
|
];
|
||||||
<option key={model} value={model}>
|
|
||||||
{model}
|
return embeddingModelProvider
|
||||||
</option>
|
? embeddingModelProvider.length > 0
|
||||||
))
|
? embeddingModelProvider.map((model) => ({
|
||||||
) : (
|
label: model,
|
||||||
<option value="" disabled selected>
|
value: model,
|
||||||
No embedding models available
|
}))
|
||||||
</option>
|
: [
|
||||||
)
|
{
|
||||||
) : (
|
label: 'No embedding models available',
|
||||||
<option value="" disabled selected>
|
value: '',
|
||||||
Invalid provider, please check backend logs
|
disabled: true,
|
||||||
</option>
|
},
|
||||||
)}
|
]
|
||||||
</select>
|
: [
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
'Invalid provider, please check backend logs',
|
||||||
|
value: '',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
})()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-white/70 text-sm">OpenAI API Key</p>
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
<input
|
OpenAI API Key
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="OpenAI API Key"
|
placeholder="OpenAI API Key"
|
||||||
defaultValue={config.openaiApiKey}
|
defaultValue={config.openaiApiKey}
|
||||||
|
@ -347,12 +406,13 @@ const SettingsDialog = ({
|
||||||
openaiApiKey: e.target.value,
|
openaiApiKey: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-white/70 text-sm">Ollama API URL</p>
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
<input
|
Ollama API URL
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Ollama API URL"
|
placeholder="Ollama API URL"
|
||||||
defaultValue={config.ollamaApiUrl}
|
defaultValue={config.ollamaApiUrl}
|
||||||
|
@ -362,12 +422,13 @@ const SettingsDialog = ({
|
||||||
ollamaApiUrl: e.target.value,
|
ollamaApiUrl: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-white/70 text-sm">GROQ API Key</p>
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
<input
|
GROQ API Key
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="GROQ API Key"
|
placeholder="GROQ API Key"
|
||||||
defaultValue={config.groqApiKey}
|
defaultValue={config.groqApiKey}
|
||||||
|
@ -377,18 +438,17 @@ const SettingsDialog = ({
|
||||||
groqApiKey: e.target.value,
|
groqApiKey: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="w-full flex items-center justify-center mt-6 text-white/70 py-6">
|
<div className="w-full flex items-center justify-center mt-6 text-black/70 dark:text-white/70 py-6">
|
||||||
<RefreshCcw className="animate-spin" />
|
<RefreshCcw className="animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="w-full mt-6 space-y-2">
|
<div className="w-full mt-6 space-y-2">
|
||||||
<p className="text-xs text-white/50">
|
<p className="text-xs text-black/50 dark:text-white/50">
|
||||||
We'll refresh the page after updating the settings.
|
We'll refresh the page after updating the settings.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -4,10 +4,16 @@ import { cn } from '@/lib/utils';
|
||||||
import { BookOpenText, Home, Search, SquarePen, Settings } from 'lucide-react';
|
import { BookOpenText, Home, Search, SquarePen, Settings } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useSelectedLayoutSegments } from 'next/navigation';
|
import { useSelectedLayoutSegments } from 'next/navigation';
|
||||||
import React, { Fragment, useState } from 'react';
|
import React, { useState, type ReactNode } from 'react';
|
||||||
import Layout from './Layout';
|
import Layout from './Layout';
|
||||||
import { Dialog, Transition } from '@headlessui/react';
|
|
||||||
import SettingsDialog from './SettingsDialog';
|
import SettingsDialog from './SettingsDialog';
|
||||||
|
import ThemeSwitcher from './theme/Switcher';
|
||||||
|
|
||||||
|
const VerticalIconContainer = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-y-3 w-full">{children}</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
||||||
const segments = useSelectedLayoutSegments();
|
const segments = useSelectedLayoutSegments();
|
||||||
|
@ -38,31 +44,35 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-20 lg:flex-col">
|
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-20 lg:flex-col">
|
||||||
<div className="flex grow flex-col items-center justify-between gap-y-5 overflow-y-auto bg-[#111111] px-2 py-8">
|
<div className="flex grow flex-col items-center justify-between gap-y-5 overflow-y-auto bg-light-secondary dark:bg-dark-secondary px-2 py-8">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<SquarePen className="text-white cursor-pointer" />
|
<SquarePen className="cursor-pointer" />
|
||||||
</a>
|
</a>
|
||||||
<div className="flex flex-col items-center gap-y-3 w-full">
|
<VerticalIconContainer>
|
||||||
{navLinks.map((link, i) => (
|
{navLinks.map((link, i) => (
|
||||||
<Link
|
<Link
|
||||||
key={i}
|
key={i}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex flex-row items-center justify-center cursor-pointer hover:bg-white/10 hover:text-white duration-150 transition w-full py-2 rounded-lg',
|
'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-white' : 'text-white/70',
|
link.active
|
||||||
|
? 'text-black dark:text-white'
|
||||||
|
: 'text-black/70 dark:text-white/70',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<link.icon />
|
<link.icon />
|
||||||
{link.active && (
|
{link.active && (
|
||||||
<div className="absolute right-0 -mr-2 h-full w-1 rounded-l-lg bg-white" />
|
<div className="absolute right-0 -mr-2 h-full w-1 rounded-l-lg bg-black dark:bg-white" />
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</VerticalIconContainer>
|
||||||
|
|
||||||
<Settings
|
<Settings
|
||||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||||
className="text-white cursor-pointer"
|
className="cursor-pointer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
isOpen={isSettingsOpen}
|
isOpen={isSettingsOpen}
|
||||||
setIsOpen={setIsSettingsOpen}
|
setIsOpen={setIsSettingsOpen}
|
||||||
|
@ -70,18 +80,20 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="fixed bottom-0 w-full z-50 flex flex-row items-center gap-x-6 bg-[#111111] px-4 py-4 shadow-sm lg:hidden">
|
<div className="fixed bottom-0 w-full z-50 flex flex-row items-center gap-x-6 bg-light-primary dark:bg-dark-primary px-4 py-4 shadow-sm lg:hidden">
|
||||||
{navLinks.map((link, i) => (
|
{navLinks.map((link, i) => (
|
||||||
<Link
|
<Link
|
||||||
href={link.href}
|
href={link.href}
|
||||||
key={i}
|
key={i}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex flex-col items-center space-y-1 text-center w-full',
|
'relative flex flex-col items-center space-y-1 text-center w-full',
|
||||||
link.active ? 'text-white' : 'text-white/70',
|
link.active
|
||||||
|
? 'text-black dark:text-white'
|
||||||
|
: 'text-black dark:text-white/70',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{link.active && (
|
{link.active && (
|
||||||
<div className="absolute top-0 -mt-4 h-1 w-full rounded-b-lg bg-white" />
|
<div className="absolute top-0 -mt-4 h-1 w-full rounded-b-lg bg-black dark:bg-white" />
|
||||||
)}
|
)}
|
||||||
<link.icon />
|
<link.icon />
|
||||||
<p className="text-xs">{link.label}</p>
|
<p className="text-xs">{link.label}</p>
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
'use client';
|
||||||
|
import { ThemeProvider } from 'next-themes';
|
||||||
|
|
||||||
|
const ThemeProviderComponent = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ThemeProvider attribute="class" enableSystem={false} defaultTheme="dark">
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeProviderComponent;
|
|
@ -0,0 +1,62 @@
|
||||||
|
'use client';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
import { SunIcon, MoonIcon, MonitorIcon } from 'lucide-react';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Select } from '../SettingsDialog';
|
||||||
|
|
||||||
|
type Theme = 'dark' | 'light' | 'system';
|
||||||
|
|
||||||
|
const ThemeSwitcher = ({ className }: { className?: string }) => {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
const isTheme = useCallback((t: Theme) => t === theme, [theme]);
|
||||||
|
|
||||||
|
const handleThemeSwitch = (theme: Theme) => {
|
||||||
|
setTheme(theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isTheme('system')) {
|
||||||
|
const preferDarkScheme = window.matchMedia(
|
||||||
|
'(prefers-color-scheme: dark)',
|
||||||
|
);
|
||||||
|
|
||||||
|
const detectThemeChange = (event: MediaQueryListEvent) => {
|
||||||
|
const theme: Theme = event.matches ? 'dark' : 'light';
|
||||||
|
setTheme(theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
preferDarkScheme.addEventListener('change', detectThemeChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
preferDarkScheme.removeEventListener('change', detectThemeChange);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isTheme, setTheme, theme]);
|
||||||
|
|
||||||
|
// Avoid Hydration Mismatch
|
||||||
|
if (!mounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className={className}
|
||||||
|
value={theme}
|
||||||
|
onChange={(e) => handleThemeSwitch(e.target.value as Theme)}
|
||||||
|
options={[
|
||||||
|
{ value: 'light', label: 'Light' },
|
||||||
|
{ value: 'dark', label: 'Dark' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeSwitcher;
|
|
@ -20,6 +20,7 @@
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"markdown-to-jsx": "^7.4.5",
|
"markdown-to-jsx": "^7.4.5",
|
||||||
"next": "14.1.4",
|
"next": "14.1.4",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-text-to-speech": "^0.14.5",
|
"react-text-to-speech": "^0.14.5",
|
||||||
|
|
|
@ -1,4 +1,17 @@
|
||||||
import type { Config } from 'tailwindcss';
|
import type { Config } from 'tailwindcss';
|
||||||
|
import type { DefaultColors } from 'tailwindcss/types/generated/colors';
|
||||||
|
|
||||||
|
const themeDark = (colors: DefaultColors) => ({
|
||||||
|
50: '#0a0a0a',
|
||||||
|
100: '#111111',
|
||||||
|
200: '#1c1c1c',
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeLight = (colors: DefaultColors) => ({
|
||||||
|
50: '#fcfcf9',
|
||||||
|
100: '#f3f3ee',
|
||||||
|
200: '#e8e8e3',
|
||||||
|
});
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
content: [
|
content: [
|
||||||
|
@ -6,8 +19,33 @@ const config: Config = {
|
||||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
],
|
],
|
||||||
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
borderColor: ({ colors }) => {
|
||||||
|
return {
|
||||||
|
light: themeLight(colors),
|
||||||
|
dark: themeDark(colors),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
colors: ({ colors }) => {
|
||||||
|
const colorsDark = themeDark(colors);
|
||||||
|
const colorsLight = themeLight(colors);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dark: {
|
||||||
|
primary: colorsDark[50],
|
||||||
|
secondary: colorsDark[100],
|
||||||
|
...colorsDark,
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
primary: colorsLight[50],
|
||||||
|
secondary: colorsLight[100],
|
||||||
|
...colorsLight,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [require('@tailwindcss/typography')],
|
plugins: [require('@tailwindcss/typography')],
|
||||||
};
|
};
|
||||||
|
|
26
ui/yarn.lock
26
ui/yarn.lock
|
@ -2244,6 +2244,11 @@ natural-compare@^1.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||||
|
|
||||||
|
next-themes@^0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a"
|
||||||
|
integrity sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==
|
||||||
|
|
||||||
next@14.1.4:
|
next@14.1.4:
|
||||||
version "14.1.4"
|
version "14.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-14.1.4.tgz#203310f7310578563fd5c961f0db4729ce7a502d"
|
resolved "https://registry.yarnpkg.com/next/-/next-14.1.4.tgz#203310f7310578563fd5c961f0db4729ce7a502d"
|
||||||
|
@ -2854,8 +2859,16 @@ streamsearch@^1.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
|
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
|
||||||
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
"string-width-cjs@npm:string-width@^4.2.0":
|
||||||
name string-width-cjs
|
version "4.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
dependencies:
|
||||||
|
emoji-regex "^8.0.0"
|
||||||
|
is-fullwidth-code-point "^3.0.0"
|
||||||
|
strip-ansi "^6.0.1"
|
||||||
|
|
||||||
|
string-width@^4.1.0:
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
@ -2919,7 +2932,14 @@ string.prototype.trimstart@^1.0.8:
|
||||||
define-properties "^1.2.1"
|
define-properties "^1.2.1"
|
||||||
es-object-atoms "^1.0.0"
|
es-object-atoms "^1.0.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^5.0.1"
|
||||||
|
|
||||||
|
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
|
Loading…
Reference in New Issue