mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 21:29:08 +00:00
Upgrade: New Home Screen for Khoj (#860)
* V1 of the new automations page Implemented: - Shareable - Editable - Suggested Cards - Create new cards - added side panel new conversation button - Implement mobile-friendly view for homepage - Fix issue of new conversations being created when selected agent is changed - Improve center of the homepage experience - Fix showing agent during first chat experience - dark mode gradient updates --------- Co-authored-by: sabaimran <narmiabas@gmail.com>
This commit is contained in:
@@ -71,8 +71,9 @@ async function openChat(slug: string, userData: UserProfile | null) {
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/chat/sessions?agent_slug=${slug}`, { method: "POST" });
|
||||
const data = await response.json();
|
||||
if (response.status == 200) {
|
||||
window.location.href = `/chat`;
|
||||
window.location.href = `/chat?conversationId=${data.conversation_id}`;
|
||||
} else if (response.status == 403 || response.status == 401) {
|
||||
window.location.href = unauthenticatedRedirectUrl;
|
||||
} else {
|
||||
@@ -294,7 +295,7 @@ export default function Agents() {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={`${styles.titleBar} text-5xl`}>
|
||||
Talk to a Specialized Agent
|
||||
Agents
|
||||
</div>
|
||||
<div className={styles.agentList}>
|
||||
Error loading agents
|
||||
@@ -307,7 +308,7 @@ export default function Agents() {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={`${styles.titleBar} text-5xl`}>
|
||||
Talk to a Specialized Agent
|
||||
Agents
|
||||
</div>
|
||||
<div className={styles.agentList}>
|
||||
<InlineLoading /> booting up your agents
|
||||
|
||||
@@ -4,10 +4,14 @@ div.main {
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
display: flex;
|
||||
overflow-x: none;
|
||||
height: 50%;
|
||||
padding: 10px;
|
||||
white-space: nowrap;
|
||||
/* grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); */
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
/* justify-content: center; */
|
||||
}
|
||||
|
||||
div.inputBox {
|
||||
@@ -124,3 +128,33 @@ div.agentIndicator {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#pink {
|
||||
background-color: #f8d1f8;
|
||||
color: #000000;
|
||||
background-image: linear-gradient(to top, #f8d1f8 1%, white 100%);
|
||||
}
|
||||
|
||||
#blue {
|
||||
background-color: #d1f8f8;
|
||||
color: #000000;
|
||||
background-image: linear-gradient(to top, #d1f8f8 1%, white 100%);
|
||||
}
|
||||
|
||||
#green {
|
||||
background-color: #d1f8d1;
|
||||
color: #000000;
|
||||
background-image: linear-gradient(to top, #d1f8d1 1%, white 100%);
|
||||
}
|
||||
|
||||
#purple {
|
||||
background-color: #f8d1f8;
|
||||
color: #000000;
|
||||
background-image: linear-gradient(to top, #f8d1f8 1%, white 100%);
|
||||
}
|
||||
|
||||
#yellow {
|
||||
background-color: #f8f8d1;
|
||||
color: #000000;
|
||||
background-image: linear-gradient(to top, #f8f8d1 1%, white 100%);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import styles from './chat.module.css';
|
||||
import React, { Suspense, useEffect, useState } from 'react';
|
||||
|
||||
import SuggestionCard from '../components/suggestions/suggestionCard';
|
||||
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
||||
import ChatHistory from '../components/chatHistory/chatHistory';
|
||||
import NavMenu from '../components/navMenu/navMenu';
|
||||
@@ -19,9 +18,6 @@ import { welcomeConsole } from '../common/utils';
|
||||
import ChatInputArea, { ChatOptions } from '../components/chatInputArea/chatInputArea';
|
||||
import { useAuthenticatedData } from '../common/auth';
|
||||
|
||||
|
||||
const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple'];
|
||||
|
||||
interface ChatBodyDataProps {
|
||||
chatOptionsData: ChatOptions | null;
|
||||
setTitle: (title: string) => void;
|
||||
@@ -40,43 +36,38 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
||||
const [processingMessage, setProcessingMessage] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversationId) {
|
||||
props.onConversationIdChange?.(conversationId);
|
||||
const storedMessage = localStorage.getItem("message");
|
||||
if (storedMessage) {
|
||||
setMessage(storedMessage);
|
||||
}
|
||||
}, [conversationId, props.onConversationIdChange]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (message) {
|
||||
if(message){
|
||||
setProcessingMessage(true);
|
||||
props.setQueryToProcess(message);
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversationId) {
|
||||
props.onConversationIdChange?.(conversationId);
|
||||
}
|
||||
}, [conversationId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.streamedMessages &&
|
||||
props.streamedMessages.length > 0 &&
|
||||
props.streamedMessages[props.streamedMessages.length - 1].completed) {
|
||||
|
||||
setProcessingMessage(false);
|
||||
} else {
|
||||
setMessage('');
|
||||
}
|
||||
}, [props.streamedMessages]);
|
||||
|
||||
if (!conversationId) {
|
||||
return (
|
||||
<div className={styles.suggestions}>
|
||||
{props.chatOptionsData && Object.entries(props.chatOptionsData).map(([key, value]) => (
|
||||
<SuggestionCard
|
||||
key={key}
|
||||
title={`/${key}`}
|
||||
body={value}
|
||||
link='#' // replace with actual link if available
|
||||
styleClass={styleClassOptions[Math.floor(Math.random() * styleClassOptions.length)]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
if(!conversationId) {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -88,7 +79,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
||||
pendingMessage={processingMessage ? message : ''}
|
||||
incomingMessages={props.streamedMessages} />
|
||||
</div>
|
||||
<div className={`${styles.inputBox} bg-background align-middle items-center justify-center px-3`}>
|
||||
<div className={`${styles.inputBox} bg-background align-middle items-center justify-center px-3 dark:bg-neutral-700 dark:border-0 dark:shadow-sm`}>
|
||||
<ChatInputArea
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
sendMessage={(message) => setMessage(message)}
|
||||
@@ -121,7 +112,6 @@ export default function Chat() {
|
||||
const handleWebSocketMessage = (event: MessageEvent) => {
|
||||
let chunk = event.data;
|
||||
let currentMessage = messages.find(message => !message.completed);
|
||||
|
||||
if (!currentMessage) {
|
||||
console.error("No current message found");
|
||||
return;
|
||||
@@ -205,51 +195,72 @@ export default function Chat() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatWS && queryToProcess) {
|
||||
// Add a new object to the state
|
||||
const newStreamMessage: StreamMessage = {
|
||||
rawResponse: "",
|
||||
trainOfThought: [],
|
||||
context: [],
|
||||
onlineContext: {},
|
||||
completed: false,
|
||||
timestamp: (new Date()).toISOString(),
|
||||
rawQuery: queryToProcess || "",
|
||||
}
|
||||
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
|
||||
setProcessQuerySignal(true);
|
||||
} else {
|
||||
if (!chatWS) {
|
||||
console.error("No WebSocket connection available");
|
||||
}
|
||||
if (!queryToProcess) {
|
||||
console.error("No query to process");
|
||||
}
|
||||
}
|
||||
}, [queryToProcess]);
|
||||
if (chatWS) {
|
||||
chatWS.onmessage = handleWebSocketMessage;
|
||||
}
|
||||
}, [chatWS, messages]);
|
||||
|
||||
//same as ChatBodyData for local storage message
|
||||
useEffect(() => {
|
||||
const storedMessage = localStorage.getItem("message");
|
||||
setQueryToProcess(storedMessage || '');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (processQuerySignal && chatWS) {
|
||||
if (chatWS && queryToProcess) {
|
||||
const newStreamMessage: StreamMessage = {
|
||||
rawResponse: "",
|
||||
trainOfThought: [],
|
||||
context: [],
|
||||
onlineContext: {},
|
||||
completed: false,
|
||||
timestamp: (new Date()).toISOString(),
|
||||
rawQuery: queryToProcess || "",
|
||||
};
|
||||
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
|
||||
|
||||
if (chatWS.readyState === WebSocket.OPEN) {
|
||||
chatWS.send(queryToProcess);
|
||||
setProcessQuerySignal(true);
|
||||
}
|
||||
else {
|
||||
console.error("WebSocket is not open. ReadyState:", chatWS.readyState);
|
||||
}
|
||||
|
||||
setQueryToProcess('');
|
||||
}
|
||||
}, [queryToProcess, chatWS]);
|
||||
|
||||
useEffect(() => {
|
||||
if (processQuerySignal && chatWS && chatWS.readyState === WebSocket.OPEN) {
|
||||
setProcessQuerySignal(false);
|
||||
chatWS.onmessage = handleWebSocketMessage;
|
||||
chatWS?.send(queryToProcess);
|
||||
chatWS.send(queryToProcess);
|
||||
localStorage.removeItem("message");
|
||||
}
|
||||
}, [processQuerySignal]);
|
||||
}, [processQuerySignal, chatWS]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (conversationId) {
|
||||
const setupWebSocketConnection = async () => {
|
||||
if (conversationId && (!chatWS || chatWS.readyState === WebSocket.CLOSED)) {
|
||||
if(queryToProcess) {
|
||||
const newWS = await setupWebSocket(conversationId, queryToProcess);
|
||||
localStorage.removeItem("message");
|
||||
setChatWS(newWS);
|
||||
}
|
||||
else {
|
||||
const newWS = await setupWebSocket(conversationId);
|
||||
setChatWS(newWS);
|
||||
}
|
||||
})();
|
||||
}
|
||||
};
|
||||
setupWebSocketConnection();
|
||||
}, [conversationId]);
|
||||
|
||||
const handleConversationIdChange = (newConversationId: string) => {
|
||||
setConversationID(newConversationId);
|
||||
};
|
||||
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
@@ -260,7 +271,7 @@ export default function Chat() {
|
||||
<title>
|
||||
{title}
|
||||
</title>
|
||||
<div className={styles.sidePanel}>
|
||||
<div>
|
||||
<SidePanel
|
||||
webSocketConnected={chatWS !== null}
|
||||
conversationId={conversationId}
|
||||
|
||||
@@ -76,7 +76,9 @@ export const setupWebSocket = async (conversationId: string, initialMessage?: st
|
||||
|
||||
let webSocketUrl = `${wsProtocol}//${host}/api/chat/ws`;
|
||||
|
||||
if (conversationId === null) return null;
|
||||
if (conversationId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (conversationId) {
|
||||
webSocketUrl += `?conversation_id=${conversationId}`;
|
||||
|
||||
60
src/interface/web/app/common/colorUtils.ts
Normal file
60
src/interface/web/app/common/colorUtils.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export function convertColorToTextClass(color: string) {
|
||||
if (color === 'red') return `text-red-500`;
|
||||
if (color === 'yellow') return `text-yellow-500`;
|
||||
if (color === 'green') return `text-green-500`;
|
||||
if (color === 'blue') return `text-blue-500`;
|
||||
if (color === 'orange') return `text-orange-500`;
|
||||
if (color === 'purple') return `text-purple-500`;
|
||||
if (color === 'pink') return `text-pink-500`;
|
||||
if (color === 'teal') return `text-teal-500`;
|
||||
if (color === 'cyan') return `text-cyan-500`;
|
||||
if (color === 'lime') return `text-lime-500`;
|
||||
if (color === 'indigo') return `text-indigo-500`;
|
||||
if (color === 'fuschia') return `text-fuschia-500`;
|
||||
if (color === 'rose') return `text-rose-500`;
|
||||
if (color === 'sky') return `text-sky-500`;
|
||||
if (color === 'amber') return `text-amber-500`;
|
||||
if (color === 'emerald') return `text-emerald-500`;
|
||||
return `text-gray-500`;
|
||||
}
|
||||
|
||||
export function convertSuggestionColorToTextClass(color: string) {
|
||||
const colors = ['blue', 'yellow', 'green', 'pink', 'purple'];
|
||||
if (colors.includes(color)) {
|
||||
return "" + `bg-gradient-to-b from-[hsl(var(--background))] to-${color}-100/${color == "green" ? "90" : "70"} dark:from-[hsl(var(--background))] dark:to-${color}-950/30 dark:border dark:border-neutral-700`;
|
||||
}
|
||||
return `bg-gradient-to-b from-white to-orange-50`;
|
||||
}
|
||||
|
||||
export function convertColorToBorderClass(color: string) {
|
||||
console.log("Color:", color);
|
||||
if (color === 'red') return `border-red-500`;
|
||||
if (color === 'yellow') return `border-yellow-500`;
|
||||
if (color === 'green') return `border-green-500`;
|
||||
if (color === 'blue') return `border-blue-500`;
|
||||
if (color === 'orange') return `border-orange-500`;
|
||||
if (color === 'purple') return `border-purple-500`;
|
||||
if (color === 'pink') return `border-pink-500`;
|
||||
if (color === 'teal') return `border-teal-500`;
|
||||
if (color === 'cyan') return `border-cyan-500`;
|
||||
if (color === 'lime') return `border-lime-500`;
|
||||
if (color === 'indigo') return `border-indigo-500`;
|
||||
if (color === 'fuschia') return `border-fuschia-500`;
|
||||
if (color === 'rose') return `border-rose-500`;
|
||||
if (color === 'sky') return `border-sky-500`;
|
||||
if (color === 'amber') return `border-amber-500`;
|
||||
if (color === 'emerald') return `border-emerald-500`;
|
||||
return `border-gray-500`;
|
||||
}
|
||||
|
||||
export const colorMap: Record<string, string> = {
|
||||
'red': 'border-red-500',
|
||||
'blue': 'border-blue-500',
|
||||
'green': 'border-green-500',
|
||||
'yellow': 'border-yellow-500',
|
||||
'purple': 'border-purple-500',
|
||||
'pink': 'border-pink-500',
|
||||
'indigo': 'border-indigo-500',
|
||||
'gray': 'border-gray-500',
|
||||
'orange': 'border-orange-500',
|
||||
};
|
||||
53
src/interface/web/app/common/iconUtils.tsx
Normal file
53
src/interface/web/app/common/iconUtils.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { convertColorToTextClass } from './colorUtils';
|
||||
import {
|
||||
Lightbulb,
|
||||
Robot,
|
||||
Aperture,
|
||||
GraduationCap,
|
||||
Jeep,
|
||||
Island,
|
||||
MathOperations,
|
||||
Asclepius,
|
||||
Couch,
|
||||
Code,
|
||||
Atom,
|
||||
ClockCounterClockwise,
|
||||
PaperPlaneTilt,
|
||||
Info,
|
||||
UserCircle,
|
||||
Globe,
|
||||
Palette,
|
||||
LinkBreak,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
interface IconMap {
|
||||
[key: string]: (color: string, width: string, height: string) => JSX.Element | null;
|
||||
}
|
||||
|
||||
const iconMap: IconMap = {
|
||||
Lightbulb: (color: string, width: string, height: string) => <Lightbulb className={`${width} ${height} ${color} mr-2`} />,
|
||||
Robot: (color: string, width: string, height: string) => <Robot className={`${width} ${height} ${color} mr-2`} />,
|
||||
Aperture: (color: string, width: string, height: string) => <Aperture className={`${width} ${height} ${color} mr-2`} />,
|
||||
GraduationCap: (color: string, width: string, height: string) => <GraduationCap className={`${width} ${height} ${color} mr-2`} />,
|
||||
Jeep: (color: string, width: string, height: string) => <Jeep className={`${width} ${height} ${color} mr-2`} />,
|
||||
Island: (color: string, width: string, height: string) => <Island className={`${width} ${height} ${color} mr-2`} />,
|
||||
MathOperations: (color: string, width: string, height: string) => <MathOperations className={`${width} ${height} ${color} mr-2`} />,
|
||||
Asclepius: (color: string, width: string, height: string) => <Asclepius className={`${width} ${height} ${color} mr-2`} />,
|
||||
Couch: (color: string, width: string, height: string) => <Couch className={`${width} ${height} ${color} mr-2`} />,
|
||||
Code: (color: string, width: string, height: string) => <Code className={`${width} ${height} ${color} mr-2`} />,
|
||||
Atom: (color: string, width: string, height: string) => <Atom className={`${width} ${height} ${color} mr-2`} />,
|
||||
ClockCounterClockwise: (color: string, width: string, height: string) => <ClockCounterClockwise className={`${width} ${height} ${color} mr-2`} />,
|
||||
Globe: (color: string, width: string, height: string) => <Globe className={`${width} ${height} ${color} mr-2`} />,
|
||||
Palette: (color: string, width: string, height: string) => <Palette className={`${width} ${height} ${color} mr-2`} />,
|
||||
};
|
||||
|
||||
function getIconFromIconName(iconName: string, color: string = 'gray', width: string = 'w-6', height: string = 'h-6') {
|
||||
const icon = iconMap[iconName];
|
||||
const colorName = color.toLowerCase();
|
||||
const colorClass = convertColorToTextClass(colorName);
|
||||
|
||||
return icon ? icon(colorClass, width, height) : null;
|
||||
}
|
||||
|
||||
export { getIconFromIconName };
|
||||
@@ -177,7 +177,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
.then(response => response.json())
|
||||
.then((chatData: ChatResponse) => {
|
||||
props.setTitle(chatData.response.slug);
|
||||
if (chatData && chatData.response && chatData.response.chat.length > 0) {
|
||||
if (chatData && chatData.response && chatData.response.chat && chatData.response.chat.length > 0) {
|
||||
|
||||
if (chatData.response.chat.length === data?.chat.length) {
|
||||
setHasMoreMessages(false);
|
||||
@@ -192,7 +192,18 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
}
|
||||
setFetchingData(false);
|
||||
} else {
|
||||
if (chatData.response.agent && chatData.response.conversation_id) {
|
||||
const chatMetadata ={
|
||||
chat: [],
|
||||
agent: chatData.response.agent,
|
||||
conversation_id: chatData.response.conversation_id,
|
||||
slug: chatData.response.slug,
|
||||
}
|
||||
setData(chatMetadata);
|
||||
}
|
||||
|
||||
setHasMoreMessages(false);
|
||||
setFetchingData(false);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -241,7 +252,6 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
if (!props.conversationId && !props.publicConversationSlug) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className={`h-[80vh]`}>
|
||||
<div ref={ref}>
|
||||
@@ -334,7 +344,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
<ProfileCard
|
||||
name={constructAgentName()}
|
||||
link={constructAgentLink()}
|
||||
avatar={<Lightbulb color='orange' weight='fill' className="mt-1 mx-1" />}
|
||||
avatar={<Lightbulb color='orange' weight='fill' />}
|
||||
description={constructAgentPersona()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,29 @@ interface ChatInputProps {
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
async function createNewConvo() {
|
||||
try {
|
||||
const response = await fetch('/api/chat/sessions', { method: "POST" });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const conversationID = data.conversation_id;
|
||||
|
||||
if (!conversationID) {
|
||||
throw new Error("Conversation ID not found in response");
|
||||
}
|
||||
|
||||
const url = `/chat?conversationId=${conversationID}`;
|
||||
return url;
|
||||
} catch (error) {
|
||||
console.error("Error creating new conversation:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default function ChatInputArea(props: ChatInputProps) {
|
||||
const [message, setMessage] = useState('');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -270,7 +293,7 @@ export default function ChatInputArea(props: ChatInputProps) {
|
||||
</Popover>
|
||||
</div>
|
||||
}
|
||||
<div className={`${styles.actualInputArea} flex items-center justify-between`}>
|
||||
<div className={`${styles.actualInputArea} flex items-center justify-between dark:bg-neutral-700`}>
|
||||
<input
|
||||
type="file"
|
||||
multiple={true}
|
||||
@@ -283,11 +306,11 @@ export default function ChatInputArea(props: ChatInputProps) {
|
||||
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
|
||||
disabled={props.sendDisabled}
|
||||
onClick={handleFileButtonClick}>
|
||||
<FileArrowUp weight='fill' />
|
||||
<FileArrowUp weight='fill' className={`${props.isMobileWidth ? 'w-6 h-6' : 'w-8 h-8'}`} />
|
||||
</Button>
|
||||
<div className="grid w-full gap-1.5 relative">
|
||||
<Textarea
|
||||
className='border-none w-full h-16 min-h-16 md:py-4 rounded-lg text-lg'
|
||||
className={`border-none w-full h-16 min-h-16 md:py-4 rounded-lg resize-none dark:bg-neutral-700 ${props.isMobileWidth ? 'text-md' : 'text-lg'}`}
|
||||
placeholder="Type / to see a list of commands"
|
||||
id="message"
|
||||
value={message}
|
||||
@@ -304,13 +327,13 @@ export default function ChatInputArea(props: ChatInputProps) {
|
||||
variant={'ghost'}
|
||||
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
|
||||
disabled={props.sendDisabled}>
|
||||
<Microphone weight='fill' />
|
||||
<Microphone weight='fill' className={`${props.isMobileWidth ? 'w-6 h-6' : 'w-8 h-8'}`} />
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-orange-300 hover:bg-orange-500 rounded-full p-0 h-auto text-3xl transition transform hover:-translate-y-1"
|
||||
onClick={onSendMessage}
|
||||
disabled={props.sendDisabled}>
|
||||
<ArrowCircleUp />
|
||||
<ArrowCircleUp className={`${props.isMobileWidth ? 'w-6 h-6' : 'w-8 h-8'}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -116,13 +116,13 @@ export default function NavMenu(props: NavMenuProps) {
|
||||
<DropdownMenuTrigger>=</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>Chat</Link>
|
||||
<Link href='/' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background no-underline`}>Chat</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>Agents</Link>
|
||||
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background no-underline`}>Agents</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>Automations</Link>
|
||||
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background no-underline`}>Automations</Link>
|
||||
</DropdownMenuItem>
|
||||
{userData && <>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -142,17 +142,17 @@ export default function NavMenu(props: NavMenuProps) {
|
||||
:
|
||||
<Menubar className='items-top inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground'>
|
||||
<MenubarMenu>
|
||||
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>
|
||||
<Link href='/' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background no-underline`}>
|
||||
<MenubarTrigger>Chat</MenubarTrigger>
|
||||
</Link>
|
||||
</MenubarMenu>
|
||||
<MenubarMenu>
|
||||
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>
|
||||
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background no-underline`}>
|
||||
<MenubarTrigger>Agents</MenubarTrigger>
|
||||
</Link>
|
||||
</MenubarMenu>
|
||||
<MenubarMenu>
|
||||
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>
|
||||
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background no-underline`}>
|
||||
<MenubarTrigger>Automations</MenubarTrigger>
|
||||
</Link>
|
||||
</MenubarMenu>
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import React from 'react';
|
||||
import { ArrowRight } from '@phosphor-icons/react';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
|
||||
interface ProfileCardProps {
|
||||
name: string;
|
||||
avatar: JSX.Element;
|
||||
avatar: JSX.Element;
|
||||
link: string;
|
||||
description?: string; // Optional description field
|
||||
}
|
||||
@@ -11,24 +20,37 @@ interface ProfileCardProps {
|
||||
const ProfileCard: React.FC<ProfileCardProps> = ({ name, avatar, link, description }) => {
|
||||
return (
|
||||
<div className="relative group flex">
|
||||
{avatar}
|
||||
<span>{name}</span>
|
||||
<div className="absolute left-0 bottom-full w-80 h-30 p-2 pb-4 bg-white border border-gray-300 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="flex items-center">
|
||||
{avatar}
|
||||
<span className="mr-2 mt-1 flex">
|
||||
{name}
|
||||
<a href={link} target="_blank" rel="noreferrer" className="mt-1 ml-2 block">
|
||||
<ArrowRight weight="bold"/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
{description && (
|
||||
<p className="mt-2 ml-6 text-sm text-gray-600 line-clamp-2">
|
||||
{description || 'A Khoj agent'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" className="flex items-center justify-center gap-2">
|
||||
{avatar}
|
||||
<div>{name}</div>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className='w-80 h-30'>
|
||||
{/* <div className="absolute left-0 bottom-full w-80 h-30 p-2 pb-4 bg-white border border-gray-300 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300"> */}
|
||||
<a href={link} target="_blank" rel="noreferrer" className="mt-1 ml-2 block no-underline">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
{avatar}
|
||||
<div className="mr-2 mt-1 flex justify-center items-center text-sm font-semibold text-gray-800">
|
||||
{name}
|
||||
<ArrowRight weight="bold" className='ml-1' />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{description && (
|
||||
<p className="mt-2 ml-6 text-sm text-gray-600 line-clamp-2">
|
||||
{description || 'A Khoj agent'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
import { ArrowRight, ArrowLeft, ArrowDown, Spinner, Check, FolderPlus, DotsThreeVertical, House, StackPlus, UserCirclePlus } from "@phosphor-icons/react";
|
||||
import { ArrowRight, ArrowLeft, ArrowDown, Spinner, Check, FolderPlus, DotsThreeVertical, House, StackPlus, UserCirclePlus, Sidebar, NotePencil } from "@phosphor-icons/react";
|
||||
|
||||
interface ChatHistory {
|
||||
conversation_id: string;
|
||||
@@ -351,7 +351,7 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
|
||||
agent_avatar={chatHistory.agent_avatar}
|
||||
agent_name={chatHistory.agent_name}
|
||||
showSidePanel={props.setEnabled}
|
||||
/>
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
@@ -548,7 +548,7 @@ function ChatSessionsModal({ data, showSidePanel }: ChatSessionsModalProps) {
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
className="flex text-left text-medium text-gray-500 hover:text-gray-300 cursor-pointer my-4 text-sm p-[0.5rem]">
|
||||
<span className="mr-2">See All <ArrowRight className="inline h-4 w-4" weight="bold"/></span>
|
||||
<span className="mr-2">See All <ArrowRight className="inline h-4 w-4" weight="bold" /></span>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -570,7 +570,7 @@ function ChatSessionsModal({ data, showSidePanel }: ChatSessionsModalProps) {
|
||||
slug={chatHistory.slug}
|
||||
agent_avatar={chatHistory.agent_avatar}
|
||||
agent_name={chatHistory.agent_name}
|
||||
showSidePanel={showSidePanel}/>
|
||||
showSidePanel={showSidePanel} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
@@ -702,8 +702,14 @@ export default function SidePanel(props: SidePanelProps) {
|
||||
|
||||
return (
|
||||
<div className={`${styles.panel} ${enabled ? styles.expanded : styles.collapsed}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<img src="/khoj-logo.svg" alt="logo" className="w-16"/>
|
||||
<div className={`flex items-center justify-between ${(enabled || props.isMobileWidth) ? 'flex-row' : 'flex-col'}`}>
|
||||
<Link href='/'>
|
||||
<img
|
||||
src="/khoj-logo.svg"
|
||||
alt="khoj logo"
|
||||
width={52}
|
||||
height={52} />
|
||||
</Link>
|
||||
{
|
||||
authenticatedData && props.isMobileWidth ?
|
||||
<Drawer open={enabled} onOpenChange={(open) => {
|
||||
@@ -711,7 +717,7 @@ export default function SidePanel(props: SidePanelProps) {
|
||||
setEnabled(open);
|
||||
}
|
||||
}>
|
||||
<DrawerTrigger><ArrowRight className="h-4 w-4 mx-2" weight="bold"/></DrawerTrigger>
|
||||
<DrawerTrigger><Sidebar className="h-4 w-4 mx-2" weight="thin" /></DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Sessions and Files</DrawerTitle>
|
||||
@@ -738,9 +744,14 @@ export default function SidePanel(props: SidePanelProps) {
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
:
|
||||
<button className={styles.button} onClick={() => setEnabled(!enabled)}>
|
||||
{enabled ? <ArrowLeft className="h-4 w-4" weight="bold"/> : <ArrowRight className="h-4 w-4 mx-2" weight="bold"/>}
|
||||
</button>
|
||||
<div className={`flex items-center ${enabled ? 'flex-row gap-2' : 'flex-col pt-2'}`}>
|
||||
<Link className={` ${enabled ? 'ml-2' : ''}`} href="/">
|
||||
{enabled ? <NotePencil className="h-6 w-6" /> : <NotePencil className="h-6 w-6" color="gray" />}
|
||||
</Link>
|
||||
<button className={styles.button} onClick={() => setEnabled(!enabled)}>
|
||||
{enabled ? <Sidebar className="h-6 w-6" /> : <Sidebar className="h-6 w-6" color="gray" />}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
@@ -769,7 +780,7 @@ export default function SidePanel(props: SidePanelProps) {
|
||||
<Button variant="ghost"><StackPlus className="h-4 w-4 mr-1" />New Conversation</Button>
|
||||
</Link>
|
||||
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`}> {/* Redirect to login page */}
|
||||
<Button variant="default"><UserCirclePlus className="h-4 w-4 mr-1"/>Sign Up</Button>
|
||||
<Button variant="default"><UserCirclePlus className="h-4 w-4 mr-1" />Sign Up</Button>
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -59,6 +59,11 @@ div.expanded {
|
||||
div.collapsed {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
height: fit-content;
|
||||
padding-top: 1rem;
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
p.session {
|
||||
@@ -119,8 +124,8 @@ div.modalSessionsList div.session {
|
||||
@media screen and (max-width: 768px) {
|
||||
div.panel {
|
||||
padding: 0.5rem;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
div.expanded {
|
||||
|
||||
@@ -1,32 +1,53 @@
|
||||
'use client'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
|
||||
import styles from "./suggestions.module.css";
|
||||
|
||||
import { getIconFromIconName } from "@/app/common/iconUtils";
|
||||
|
||||
|
||||
function convertSuggestionColorToIconClass(color: string) {
|
||||
if (color.includes('blue')) return getIconFromIconName('Robot', 'blue', 'w-8', 'h-8');
|
||||
if (color.includes('yellow')) return getIconFromIconName('Globe', 'yellow', 'w-8', 'h-8');
|
||||
if (color.includes('green')) return getIconFromIconName('Palette', 'green', 'w-8', 'h-8');
|
||||
else return getIconFromIconName('Lightbulb', 'orange', 'w-8', 'h-8');
|
||||
}
|
||||
|
||||
|
||||
interface SuggestionCardProps {
|
||||
title: string;
|
||||
body: string;
|
||||
link: string;
|
||||
styleClass: string;
|
||||
title: string;
|
||||
body: string;
|
||||
link: string;
|
||||
image: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export default function SuggestionCard(data: SuggestionCardProps) {
|
||||
const cardClassName = `${styles.card} ${data.color} md:w-full md:h-fit sm:w-full sm:h-fit lg:w-[200px] lg:h-[200px]`;
|
||||
const titleClassName = `${styles.title} pt-2 dark:text-white dark:font-bold`;
|
||||
const descriptionClassName = `${styles.text} dark:text-white`;
|
||||
|
||||
return (
|
||||
<div className={styles[data.styleClass] + " " + styles.card}>
|
||||
<div className={styles.title}>
|
||||
{data.title}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{data.body}
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href={data.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>click me
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const cardContent = (
|
||||
<Card className={cardClassName}>
|
||||
<CardHeader className="m-0 p-2 pb-1 relative">
|
||||
{convertSuggestionColorToIconClass(data.image)}
|
||||
<CardTitle className={titleClassName}>{data.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="m-0 p-2 pr-4 pt-1">
|
||||
<CardDescription className={descriptionClassName}>{data.body}</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return data.link ? (
|
||||
<a href={data.link} className="no-underline">
|
||||
{cardContent}
|
||||
</a>
|
||||
) : cardContent;
|
||||
}
|
||||
|
||||
@@ -1,40 +1,18 @@
|
||||
div.pink {
|
||||
background-color: #f8d1f8;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
div.blue {
|
||||
background-color: #d1f8f8;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
div.green {
|
||||
background-color: #d1f8d1;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
div.purple {
|
||||
background-color: #f8d1f8;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
div.yellow {
|
||||
background-color: #f8f8d1;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
div.card {
|
||||
padding: 1rem;
|
||||
margin: 1rem;
|
||||
border: 1px solid #000000;
|
||||
.card {
|
||||
padding: 0.5rem;
|
||||
margin: 0.05rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
div.title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
.title {
|
||||
font-size: 1.0rem;
|
||||
}
|
||||
|
||||
div.body {
|
||||
font-size: 1rem;
|
||||
.text {
|
||||
padding-top: 0.2rem;
|
||||
font-size: 0.9rem;
|
||||
white-space: wrap;
|
||||
padding-right: 4px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import "./globals.css";
|
||||
const inter = Noto_Sans({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI",
|
||||
description: "An AI copilot for your second brain",
|
||||
title: "Khoj AI - Chat",
|
||||
description: "Use this page to chat with Khoj AI.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -16,7 +16,18 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
<meta httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://assets.khoj.dev;
|
||||
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
|
||||
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
|
||||
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
|
||||
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
|
||||
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
|
||||
child-src 'none';
|
||||
object-src 'none';"></meta>
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,224 +1,121 @@
|
||||
.main {
|
||||
div.main {
|
||||
height: 100vh;
|
||||
color: hsla(var(--foreground));
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6rem;
|
||||
min-height: 100vh;
|
||||
overflow-x: none;
|
||||
height: fit-content;
|
||||
padding: 10px;
|
||||
white-space: nowrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inherit;
|
||||
justify-content: inherit;
|
||||
align-items: inherit;
|
||||
font-size: 0.85rem;
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
font-family: var(--font-mono);
|
||||
div.inputBox {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 10px var(--box-shadow-color);
|
||||
margin-bottom: 20px;
|
||||
gap: 12px;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.description a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
input.inputBox {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.description p {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background-color: rgba(var(--callout-rgb), 0.5);
|
||||
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
|
||||
border-radius: var(--border-radius);
|
||||
input.inputBox:focus {
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
div.inputBox:focus {
|
||||
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.grid {
|
||||
div.chatBodyFull {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(25%, auto));
|
||||
max-width: 100%;
|
||||
width: var(--max-width);
|
||||
grid-template-columns: 1fr;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
background: rgba(var(--card-rgb), 0);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0);
|
||||
transition: background 200ms, border 200ms;
|
||||
button.inputBox {
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: linear-gradient(var(--calm-green), var(--calm-blue));
|
||||
}
|
||||
|
||||
.card span {
|
||||
display: inline-block;
|
||||
transition: transform 200ms;
|
||||
div.chatBody {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.7rem;
|
||||
.inputBox {
|
||||
color: hsla(var(--foreground));
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
max-width: 30ch;
|
||||
text-wrap: balance;
|
||||
div.chatLayout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 4rem 0;
|
||||
div.chatBox {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
background: var(--secondary-glow);
|
||||
border-radius: 50%;
|
||||
width: 480px;
|
||||
height: 360px;
|
||||
margin-left: -400px;
|
||||
div.titleBar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.center::after {
|
||||
background: var(--primary-glow);
|
||||
width: 240px;
|
||||
height: 180px;
|
||||
z-index: -1;
|
||||
div.chatBoxBody {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.center::before,
|
||||
.center::after {
|
||||
content: "";
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
filter: blur(45px);
|
||||
transform: translateZ(0);
|
||||
|
||||
div.sidePanel {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.card:hover {
|
||||
background: rgba(var(--card-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0.15);
|
||||
@media (max-width: 768px) {
|
||||
div.chatBody {
|
||||
grid-template-columns: 0fr 1fr;
|
||||
}
|
||||
|
||||
.card:hover span {
|
||||
transform: translateX(4px);
|
||||
div.chatBox {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.card:hover span {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 700px) {
|
||||
.content {
|
||||
padding: 4rem;
|
||||
@media screen and (max-width: 768px) {
|
||||
div.inputBox {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 120px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 2.5rem;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
padding: 8rem 0 6rem;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
transform: none;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.description a {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.description p,
|
||||
.description div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
div.chatBoxBody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description p {
|
||||
align-items: center;
|
||||
inset: 0 0 auto;
|
||||
padding: 2rem 1rem 1.4rem;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(var(--background-start-rgb), 1),
|
||||
rgba(var(--callout-rgb), 0.5));
|
||||
background-clip: padding-box;
|
||||
backdrop-filter: blur(24px);
|
||||
div.chatBox {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.description div {
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
inset: auto 0 0;
|
||||
padding: 2rem;
|
||||
height: 200px;
|
||||
background: linear-gradient(to bottom,
|
||||
transparent 0%,
|
||||
rgb(var(--background-end-rgb)) 40%);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet and Smaller Desktop */
|
||||
@media (min-width: 701px) and (max-width: 1120px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.logo {
|
||||
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
div.chatLayout {
|
||||
gap: 0;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,355 @@
|
||||
import styles from "./page.module.css";
|
||||
'use client'
|
||||
import './globals.css';
|
||||
|
||||
import styles from './page.module.css';
|
||||
import React, { Suspense, useEffect, useState, useMemo } from 'react';
|
||||
|
||||
import SuggestionCard from './components/suggestions/suggestionCard';
|
||||
import SidePanel from './components/sidePanel/chatHistorySidePanel';
|
||||
import NavMenu from './components/navMenu/navMenu';
|
||||
import Loading from './components/loading/loading';
|
||||
import useSWR from 'swr';
|
||||
import Image from 'next/image';
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
import { StreamMessage } from './components/chatMessage/chatMessage';
|
||||
import ChatInputArea, { ChatOptions } from './components/chatInputArea/chatInputArea';
|
||||
import { useAuthenticatedData } from './common/auth';
|
||||
import { Card, CardContent, CardTitle } from '@/components/ui/card';
|
||||
import { convertSuggestionColorToTextClass, colorMap, convertColorToBorderClass } from './common/colorUtils';
|
||||
import { getIconFromIconName } from './common/iconUtils';
|
||||
import { ClockCounterClockwise } from '@phosphor-icons/react';
|
||||
|
||||
//samples for suggestion cards (should be moved to json later)
|
||||
const suggestions: Suggestion[] = [["Automation", "blue", "Send me a summary of HackerNews every morning.", "/automations?subject=Summarizing%20Top%20Headlines%20from%20HackerNews&query=Summarize%20the%20top%20headlines%20on%20HackerNews&crontime=00%207%20*%20*%20*"], ["Automation", "blue", "Compose a bedtime story that a five-year-old might enjoy.", "/automations?subject=Daily%20Bedtime%20Story&query=Compose%20a%20bedtime%20story%20that%20a%20five-year-old%20might%20enjoy.%20It%20should%20not%20exceed%20five%20paragraphs.%20Appeal%20to%20the%20imagination%2C%20but%20weave%20in%20learnings.&crontime=0%2021%20*%20*%20*"], ["Paint", "green", "Paint a picture of a sunset but it's made of stained glass tiles", ""], ["Online Search", "yellow", "Search for the best attractions in Austria Hungary", ""]];
|
||||
|
||||
export interface AgentData {
|
||||
slug: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
personality: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface ChatBodyDataProps {
|
||||
chatOptionsData: ChatOptions | null;
|
||||
setTitle: (title: string) => void;
|
||||
onConversationIdChange?: (conversationId: string) => void;
|
||||
setQueryToProcess: (query: string) => void;
|
||||
streamedMessages: StreamMessage[];
|
||||
setUploadedFiles: (files: string[]) => void;
|
||||
isMobileWidth?: boolean;
|
||||
isLoggedIn: boolean;
|
||||
conversationId: string | null; // Added this line
|
||||
}
|
||||
type Suggestion = [string, string, string, string];
|
||||
|
||||
async function createNewConvo(slug: string) {
|
||||
try {
|
||||
const response = await fetch(`/api/chat/sessions?client=web&agent_slug=${slug}`, { method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const conversationID = data.conversation_id;
|
||||
if (!conversationID) {
|
||||
throw new Error("Conversation ID not found in response");
|
||||
}
|
||||
return conversationID;
|
||||
} catch (error) {
|
||||
console.error("Error creating new conversation:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function ChatBodyData(props: ChatBodyDataProps) {
|
||||
const [message, setMessage] = useState('');
|
||||
const [processingMessage, setProcessingMessage] = useState(false);
|
||||
const [shuffledOptions, setShuffledOptions] = useState<Suggestion[]>([]);
|
||||
const [shuffledColors, setShuffledColors] = useState<string[]>([]);
|
||||
const [selectedAgent, setSelectedAgent] = useState<string | null>("khoj");
|
||||
|
||||
const agentsFetcher = () => window.fetch('/api/agents').then(res => res.json()).catch(err => console.log(err));
|
||||
const { data, error } = useSWR<AgentData[]>('agents', agentsFetcher, { revalidateOnFocus: false });
|
||||
|
||||
function shuffleAndSetOptions() {
|
||||
const shuffled = [...suggestions].sort(() => 0.5 - Math.random());
|
||||
setShuffledOptions(shuffled.slice(0, 3));
|
||||
//use the text to color function above convertSuggestionColorToTextClass
|
||||
const colors = shuffled.map(option => convertSuggestionColorToTextClass(option[1]));
|
||||
setShuffledColors(colors);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (props.chatOptionsData) {
|
||||
shuffleAndSetOptions();
|
||||
}
|
||||
}, [props.chatOptionsData]);
|
||||
|
||||
function onButtonClick() {
|
||||
shuffleAndSetOptions();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const processMessage = async () => {
|
||||
if (message && !processingMessage) {
|
||||
setProcessingMessage(true);
|
||||
try {
|
||||
const newConversationId = await createNewConvo(selectedAgent || "khoj");
|
||||
props.onConversationIdChange?.(newConversationId);
|
||||
window.location.href = `/chat?conversationId=${newConversationId}`;
|
||||
localStorage.setItem('message', message);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error creating new conversation:", error);
|
||||
setProcessingMessage(false);
|
||||
}
|
||||
setMessage('');
|
||||
}
|
||||
};
|
||||
processMessage();
|
||||
if (message) {
|
||||
setProcessingMessage(true);
|
||||
props.setQueryToProcess(message);
|
||||
};
|
||||
}, [selectedAgent, message]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.streamedMessages &&
|
||||
props.streamedMessages.length > 0 &&
|
||||
props.streamedMessages[props.streamedMessages.length - 1].completed) {
|
||||
setProcessingMessage(false);
|
||||
} else {
|
||||
setMessage('');
|
||||
}
|
||||
}, [props.streamedMessages]);
|
||||
|
||||
const nSlice = props.isMobileWidth ? 3 : 4;
|
||||
|
||||
const agents = data ? data.slice(0, nSlice) : []; //select first 4 agents to show as options
|
||||
|
||||
//generate colored icons for the selected agents
|
||||
const agentIcons = agents.map(agent => getIconFromIconName(agent.icon, agent.color) || <Image key={agent.name} src={agent.avatar} alt={agent.name} width={50} height={50} />);
|
||||
function fillArea(link: string, type: string, prompt: string) {
|
||||
if (!link) {
|
||||
let message_str = "";
|
||||
prompt = prompt.charAt(0).toLowerCase() + prompt.slice(1);
|
||||
|
||||
if (type === "Online Search") {
|
||||
message_str = "/online " + prompt;
|
||||
} else if (type === "Paint") {
|
||||
message_str = "/paint " + prompt;
|
||||
} else {
|
||||
message_str = prompt;
|
||||
}
|
||||
// Get the textarea element
|
||||
const message_area = document.getElementById("message") as HTMLTextAreaElement;
|
||||
|
||||
if (message_area) {
|
||||
// Update the value directly
|
||||
message_area.value = message_str;
|
||||
setMessage(message_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTailwindBorderClass(color: string): string {
|
||||
return colorMap[color] || 'border-black'; // Default to black if color not found
|
||||
}
|
||||
|
||||
function highlightHandler(slug: string): void {
|
||||
const buttons = document.getElementsByClassName("agent");
|
||||
const agent = agents.find(agent => agent.slug === slug);
|
||||
const borderColorClass = getTailwindBorderClass(agent?.color || 'gray');
|
||||
|
||||
Array.from(buttons).forEach((button: Element) => {
|
||||
const buttonElement = button as HTMLElement;
|
||||
if (buttonElement.classList.contains(slug)) {
|
||||
buttonElement.classList.add(borderColorClass, 'border');
|
||||
buttonElement.classList.remove('border-stone-100', 'dark:border-neutral-700');
|
||||
}
|
||||
else {
|
||||
Object.values(colorMap).forEach(colorClass => {
|
||||
buttonElement.classList.remove(colorClass, 'border');
|
||||
});
|
||||
buttonElement.classList.add('border', 'border-stone-100', 'dark:border-neutral-700');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.chatBoxBody}`}>
|
||||
<div className="w-full text-center">
|
||||
<div className="items-center">
|
||||
<h1 className="text-center pb-6 px-4">What would you like to do?</h1>
|
||||
</div>
|
||||
{
|
||||
!props.isMobileWidth &&
|
||||
<div className="flex pb-6 gap-2 items-center justify-center">
|
||||
{agentIcons.map((icon, index) => (
|
||||
<Card
|
||||
key={`${index}-${agents[index].slug}`}
|
||||
className={
|
||||
`${selectedAgent === agents[index].slug ?
|
||||
convertColorToBorderClass(agents[index].color) : 'border-stone-100 text-muted-foreground'}
|
||||
hover:cursor-pointer rounded-lg px-2 py-2`}>
|
||||
<CardTitle
|
||||
className='text-center text-md font-medium flex justify-center items-center'
|
||||
onClick={() => setSelectedAgent(agents[index].slug)}>
|
||||
{icon} {agents[index].name}
|
||||
</CardTitle>
|
||||
</Card>
|
||||
))}
|
||||
<Card className='border-none shadow-none flex justify-center items-center hover:cursor-pointer' onClick={() => window.location.href = "/agents"}>
|
||||
<CardTitle className="text-center text-md font-normal flex justify-center items-center px-1.5 py-2">See All →</CardTitle>
|
||||
</Card>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={`${props.isMobileWidth} ? 'w-full' : 'w-fit`}>
|
||||
{
|
||||
!props.isMobileWidth &&
|
||||
<div className={`${styles.inputBox} bg-background align-middle items-center justify-center p-3 dark:bg-neutral-700`}>
|
||||
<ChatInputArea
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
sendMessage={(message) => setMessage(message)}
|
||||
sendDisabled={processingMessage}
|
||||
chatOptionsData={props.chatOptionsData}
|
||||
conversationId={null}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
setUploadedFiles={props.setUploadedFiles} />
|
||||
</div>
|
||||
}
|
||||
<div className={`suggestions ${styles.suggestions} w-full ${props.isMobileWidth ? 'flex flex-col' : 'flex flex-row'} justify-center items-center`}>
|
||||
{shuffledOptions.map(([key, styleClass, value, link], index) => (
|
||||
<div key={key} onClick={() => fillArea(link, key, value)}>
|
||||
<SuggestionCard
|
||||
key={key + Math.random()}
|
||||
title={key}
|
||||
body={value.length > 65 ? value.substring(0, 65) + '...' : value}
|
||||
link={link}
|
||||
color={shuffledColors[index]}
|
||||
image={shuffledColors[index]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-center margin-auto">
|
||||
<button
|
||||
onClick={onButtonClick}
|
||||
className="m-2 p-1.5 rounded-lg dark:hover:bg-[var(--background-color)] hover:bg-stone-100 border border-stone-100 text-sm text-stone-500 dark:text-stone-300 dark:border-neutral-700">
|
||||
More Examples <ClockCounterClockwise className='h-4 w-4 inline' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
props.isMobileWidth &&
|
||||
<div className={`${styles.inputBox} dark:bg-neutral-700 bg-background dark: align-middle items-center justify-center py-3 px-1`}>
|
||||
<ChatInputArea
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
sendMessage={(message) => setMessage(message)}
|
||||
sendDisabled={processingMessage}
|
||||
chatOptionsData={props.chatOptionsData}
|
||||
conversationId={null}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
setUploadedFiles={props.setUploadedFiles} />
|
||||
<div className="flex gap-2 items-center justify-left pt-4">
|
||||
{agentIcons.map((icon, index) => (
|
||||
<Card
|
||||
key={`${index}-${agents[index].slug}`}
|
||||
className={
|
||||
`${selectedAgent === agents[index].slug ? convertColorToBorderClass(agents[index].color) : 'border-muted text-muted-foreground'} hover:cursor-pointer`
|
||||
}>
|
||||
<CardTitle
|
||||
className='text-center text-xs font-medium flex justify-center items-center px-1.5 py-2'
|
||||
onClick={() => setSelectedAgent(agents[index].slug)}>
|
||||
{icon} {agents[index].name}
|
||||
</CardTitle>
|
||||
</Card>
|
||||
))}
|
||||
<Card className='border-none shadow-none flex justify-center items-center hover:cursor-pointer' onClick={() => window.location.href = "/agents"}>
|
||||
<CardTitle className={`text-center ${props.isMobileWidth ? 'text-xs' : 'text-md'} font-normal flex justify-center items-center px-1.5 py-2`}>See All →</CardTitle>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className={`${styles.main} text-3xl font-bold underline`}>
|
||||
Hi, Khoj here.
|
||||
</main>
|
||||
);
|
||||
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [title, setTitle] = useState('');
|
||||
const [conversationId, setConversationID] = useState<string | null>(null);
|
||||
const [messages, setMessages] = useState<StreamMessage[]>([]);
|
||||
const [queryToProcess, setQueryToProcess] = useState<string>('');
|
||||
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||
|
||||
const authenticatedData = useAuthenticatedData();
|
||||
|
||||
const handleConversationIdChange = (newConversationId: string) => {
|
||||
setConversationID(newConversationId);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/chat/options')
|
||||
.then(response => response.json())
|
||||
.then((data: ChatOptions) => {
|
||||
setLoading(false);
|
||||
if (data) {
|
||||
setChatOptionsData(data);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
|
||||
setIsMobileWidth(window.innerWidth < 786);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
setIsMobileWidth(window.innerWidth < 786);
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.main} ${styles.chatLayout}`}>
|
||||
<title>
|
||||
{title}
|
||||
</title>
|
||||
<div className={`${styles.sidePanel}`}>
|
||||
<SidePanel
|
||||
webSocketConnected={true}
|
||||
conversationId={conversationId}
|
||||
uploadedFiles={uploadedFiles}
|
||||
isMobileWidth={isMobileWidth}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles.chatBox}`}>
|
||||
<NavMenu selected="Chat" title={title}></NavMenu>
|
||||
<div className={`${styles.chatBoxBody}`}>
|
||||
<ChatBodyData
|
||||
isLoggedIn={authenticatedData !== null}
|
||||
streamedMessages={messages}
|
||||
chatOptionsData={chatOptionsData}
|
||||
setTitle={setTitle}
|
||||
setQueryToProcess={setQueryToProcess}
|
||||
setUploadedFiles={setUploadedFiles}
|
||||
isMobileWidth={isMobileWidth}
|
||||
onConversationIdChange={handleConversationIdChange}
|
||||
conversationId={conversationId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
30
src/interface/web/components/ui/tooltip.tsx
Normal file
30
src/interface/web/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
9216
src/interface/web/package-lock.json
generated
Normal file
9216
src/interface/web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -35,19 +35,16 @@
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@radix-ui/themes": "^3.1.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"cronstrue": "^2.50.0",
|
||||
"dompurify": "^3.1.6",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"clsx": "^2.1.1",
|
||||
"cronstrue": "^2.50.0",
|
||||
"katex": "^0.16.10",
|
||||
"lucide-react": "^0.397.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
@@ -60,17 +57,12 @@
|
||||
"react-hook-form": "^7.52.1",
|
||||
"shadcn-ui": "^0.8.0",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.1",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
@@ -79,7 +71,15 @@
|
||||
"lint-staged": "^15.2.7",
|
||||
"nodemon": "^3.1.3",
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "^5"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"typescript": "^5",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 4
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { Config } from "tailwindcss"
|
||||
|
||||
const config = {
|
||||
safelist: [
|
||||
{
|
||||
pattern: /to-(blue|yellow|green|pink|purple)-(50|100|200|950)/,
|
||||
variants: ['dark'],
|
||||
},
|
||||
],
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user