mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 13:19:16 +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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user