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

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