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:
Raghav Tirumale
2024-07-24 03:46:19 -04:00
committed by GitHub
parent 9cf52bb7e4
commit 3e4325edab
23 changed files with 10964 additions and 1039 deletions

View File

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

View File

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

View File

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

View File

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

View 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',
};

View 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 };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

File diff suppressed because it is too large Load Diff

View File

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

View File

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