New Agents Page User Interface (#866)

Changes for new agents page
- Modernized agent cards
- Responsive design to support mobile users
- Button for users to create their own agents (coming soon)
- Optimized to use tailwind and icon utils
- Side panel added for quick access to conversations
This commit is contained in:
Raghav Tirumale
2024-07-26 10:42:31 -04:00
committed by GitHub
parent 52db15706d
commit 5dcac18ba5
8 changed files with 1095 additions and 343 deletions

View File

@@ -6,7 +6,7 @@ div.titleBar {
.agentPersonality p { .agentPersonality p {
white-space: inherit; white-space: inherit;
overflow: hidden; overflow: hidden;
height: 78px; height: 77px;
line-height: 1.5; line-height: 1.5;
} }
@@ -16,6 +16,16 @@ div.agentPersonality {
overflow: hidden; overflow: hidden;
} }
div.sidePanel {
position: fixed;
height: 100%;
}
div.chatLayout {
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
}
button.infoButton { button.infoButton {
@@ -30,7 +40,7 @@ button.infoButton {
div.agentList { div.agentList {
display: grid; display: grid;
gap: 20px; gap: 20px;
padding: 20px; padding-top: 30px;
margin-right: auto; margin-right: auto;
grid-auto-flow: row; grid-auto-flow: row;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@@ -40,7 +50,7 @@ div.agentList {
@media only screen and (max-width: 700px) { @media only screen and (max-width: 700px) {
div.agentList { div.agentList {
width: 90%; width: 100%;
padding: 0; padding: 0;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;

View File

@@ -1,5 +1,5 @@
.agentsLayout { .agentsLayout {
max-width: 70vw; max-width: 100vw;
margin: auto; margin: auto;
} }

View File

@@ -1,16 +1,12 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import NavMenu from '../components/navMenu/navMenu'; import { Noto_Sans } from "next/font/google";
import styles from './agentsLayout.module.css';
import "../globals.css"; import "../globals.css";
const inter = Noto_Sans({ subsets: ["latin"] });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Khoj AI - Agents", title: "Khoj AI - Chat",
description: "Use Agents with Khoj AI for deeper, more personalized queries.", description: "Use this page to chat with Khoj AI.",
icons: {
icon: '/static/favicon.ico',
},
}; };
export default function RootLayout({ export default function RootLayout({
@@ -19,9 +15,20 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<div className={`${styles.agentsLayout}`}> <html lang="en">
<NavMenu selected="Agents" showLogo={true} /> <meta httpEquiv="Content-Security-Policy"
content="default-src 'self' https://assets.khoj.dev;
media-src * blob:;
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} {children}
</div> </body>
</html>
); );
} }

View File

@@ -3,7 +3,6 @@
import styles from './agents.module.css'; import styles from './agents.module.css';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link';
import useSWR from 'swr'; import useSWR from 'swr';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -11,47 +10,31 @@ import { useEffect, useState } from 'react';
import { useAuthenticatedData, UserProfile } from '../common/auth'; import { useAuthenticatedData, UserProfile } from '../common/auth';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Lightbulb, AlertDialog,
Robot, AlertDialogAction,
Aperture, AlertDialogContent,
GraduationCap, AlertDialogDescription,
Jeep, AlertDialogFooter,
Island, AlertDialogHeader,
MathOperations, AlertDialogTitle,
Asclepius, AlertDialogTrigger,
Couch, } from "@/components/ui/alert-dialog"
Code,
Atom, import {
ClockCounterClockwise,
PaperPlaneTilt, PaperPlaneTilt,
Info, Lightning,
UserCircle Plus,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTrigger } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTrigger } from '@/components/ui/dialog';
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer'; import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer';
import LoginPrompt from '../components/loginPrompt/loginPrompt'; import LoginPrompt from '../components/loginPrompt/loginPrompt';
import Loading, { InlineLoading } from '../components/loading/loading'; import { InlineLoading } from '../components/loading/loading';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import SidePanel from '../components/sidePanel/chatHistorySidePanel';
import NavMenu from '../components/navMenu/navMenu';
interface IconMap { import { getIconFromIconName } from '../common/iconUtils';
[key: string]: (color: string, width: string, height: string) => JSX.Element | null; import { convertColorToTextClass } from '../common/colorUtils';
}
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`} />,
};
export interface AgentData { export interface AgentData {
slug: string; slug: string;
@@ -83,62 +66,12 @@ async function openChat(slug: string, userData: UserProfile | null) {
const agentsFetcher = () => window.fetch('/api/agents').then(res => res.json()).catch(err => console.log(err)); const agentsFetcher = () => window.fetch('/api/agents').then(res => res.json()).catch(err => console.log(err));
interface AgentCardProps { interface AgentCardProps {
data: AgentData; data: AgentData;
userProfile: UserProfile | null; userProfile: UserProfile | null;
isMobileWidth: boolean; isMobileWidth: boolean;
} }
function getIconFromIconName(iconName: string, color: string = 'gray', width: string = 'w-8', height: string = 'h-8') {
const icon = iconMap[iconName];
const colorName = color.toLowerCase();
const colorClass = convertColorToTextClass(colorName);
return icon ? icon(colorClass, width, height) : null;
}
function convertColorToClass(color: string) {
// We can't dyanmically generate the classes for tailwindcss, so we have to explicitly use the whole string.
// See models/__init__.py 's definition of the Agent model for the color choices.
if (color === 'red') return `bg-red-500 hover:bg-red-600`;
if (color === 'yellow') return `bg-yellow-500 hover:bg-yellow-600`;
if (color === 'green') return `bg-green-500 hover:bg-green-600`;
if (color === 'blue') return `bg-blue-500 hover:bg-blue-600`;
if (color === 'orange') return `bg-orange-500 hover:bg-orange-600`;
if (color === 'purple') return `bg-purple-500 hover:bg-purple-600`;
if (color === 'pink') return `bg-pink-500 hover:bg-pink-600`;
if (color === 'teal') return `bg-teal-500 hover:bg-teal-600`;
if (color === 'cyan') return `bg-cyan-500 hover:bg-cyan-600`;
if (color === 'lime') return `bg-lime-500 hover:bg-lime-600`;
if (color === 'indigo') return `bg-indigo-500 hover:bg-indigo-600`;
if (color === 'fuschia') return `bg-fuschia-500 hover:bg-fuschia-600`;
if (color === 'rose') return `bg-rose-500 hover:bg-rose-600`;
if (color === 'sky') return `bg-sky-500 hover:bg-sky-600`;
if (color === 'amber') return `bg-amber-500 hover:bg-amber-600`;
if (color === 'emerald') return `bg-emerald-500 hover:bg-emerald-600`;
return `bg-gray-500 hover:bg-gray-600`;
}
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`;
}
function AgentCard(props: AgentCardProps) { function AgentCard(props: AgentCardProps) {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
@@ -152,10 +85,10 @@ function AgentCard(props: AgentCardProps) {
window.history.pushState({}, `Khoj AI - Agent ${props.data.slug}`, `/agents?agent=${props.data.slug}`); window.history.pushState({}, `Khoj AI - Agent ${props.data.slug}`, `/agents?agent=${props.data.slug}`);
} }
const stylingString = convertColorToClass(props.data.color); const stylingString = convertColorToTextClass(props.data.color);
return ( return (
<Card className='shadow-md bg-secondary rounded-lg hover:shadow-lg'> <Card className={`shadow-sm bg-gradient-to-b from-white 20% to-${props.data.color ? props.data.color : "gray"}-100/50 dark:from-[hsl(var(--background))] dark:to-${props.data.color ? props.data.color : "gray"}-950/50 rounded-xl hover:shadow-md`}>
{ {
showLoginPrompt && showLoginPrompt &&
<LoginPrompt <LoginPrompt
@@ -173,7 +106,7 @@ function AgentCard(props: AgentCardProps) {
window.history.pushState({}, `Khoj AI - Agents`, `/agents`); window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
}}> }}>
<DialogTrigger> <DialogTrigger>
<div className='flex items-center'> <div className='flex items-center relative top-2'>
{ {
getIconFromIconName(props.data.icon, props.data.color) || <Image getIconFromIconName(props.data.icon, props.data.color) || <Image
src={props.data.avatar} src={props.data.avatar}
@@ -185,7 +118,22 @@ function AgentCard(props: AgentCardProps) {
{props.data.name} {props.data.name}
</div> </div>
</DialogTrigger> </DialogTrigger>
<DialogContent className='whitespace-pre-line'> <div className="float-right">
{props.userProfile ? (
<Button
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100 dark:hover:bg-neutral-900`}
onClick={() => openChat(props.data.slug, userData)}>
<PaperPlaneTilt className='w-6 h-6' color={props.data.color} />
</Button>
) : (
<Button
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100 dark:hover:bg-neutral-900`}
onClick={() => setShowLoginPrompt(true)}>
<PaperPlaneTilt className='w-6 h-6' color={props.data.color} />
</Button>
)}
</div>
<DialogContent className='whitespace-pre-line max-h-[80vh]'>
<DialogHeader> <DialogHeader>
<div className='flex items-center'> <div className='flex items-center'>
{ {
@@ -196,18 +144,21 @@ function AgentCard(props: AgentCardProps) {
height={50} height={50}
/> />
} }
{props.data.name} <p className="font-bold text-lg">{props.data.name}</p>
</div> </div>
</DialogHeader> </DialogHeader>
<div className="max-h-[60vh] overflow-y-scroll text-neutral-500 dark:text-white">
{props.data.personality} {props.data.personality}
</div>
<DialogFooter> <DialogFooter>
<Button <Button
className={`${stylingString}`} className={`pt-6 pb-6 ${stylingString} bg-white dark:bg-[hsl(var(--background))] text-neutral-500 dark:text-white border-2 border-stone-100 shadow-sm rounded-xl hover:bg-stone-100 dark:hover:bg-neutral-900 dark:border-neutral-700`}
onClick={() => { onClick={() => {
openChat(props.data.slug, userData); openChat(props.data.slug, userData);
setShowModal(false); setShowModal(false);
}}> }}>
Chat <PaperPlaneTilt className='mr-2 w-6 h-6' color={props.data.color} />
Start Chatting
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
@@ -232,6 +183,21 @@ function AgentCard(props: AgentCardProps) {
{props.data.name} {props.data.name}
</div> </div>
</DrawerTrigger> </DrawerTrigger>
<div className="float-right">
{props.userProfile ? (
<Button
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100`}
onClick={() => openChat(props.data.slug, userData)}>
<PaperPlaneTilt className='w-6 h-6' color={props.data.color} />
</Button>
) : (
<Button
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm`}
onClick={() => setShowLoginPrompt(true)}>
<PaperPlaneTilt className='w-6 h-6' color={props.data.color} />
</Button>
)}
</div>
<DrawerContent className='whitespace-pre-line p-2'> <DrawerContent className='whitespace-pre-line p-2'>
<DrawerHeader> <DrawerHeader>
<DrawerTitle>{props.data.name}</DrawerTitle> <DrawerTitle>{props.data.name}</DrawerTitle>
@@ -250,31 +216,20 @@ function AgentCard(props: AgentCardProps) {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className={styles.agentPersonality}> <div className={styles.agentPersonality}>
<button className={styles.infoButton} onClick={() => setShowModal(true)}> <button className={`${styles.infoButton} text-neutral-500 dark:text-white`} onClick={() => setShowModal(true)}>
<p>{props.data.personality}</p> <p>{props.data.personality}</p>
</button> </button>
</div> </div>
</CardContent> </CardContent>
<CardFooter className='flex justify-end'>
{
props.userProfile ?
<Button
className={`${stylingString}`}
onClick={() => openChat(props.data.slug, userData)}>
<PaperPlaneTilt className='w-6 h-6' />
</Button>
:
<Button
className={`${stylingString}`}
onClick={() => setShowLoginPrompt(true)}>
<PaperPlaneTilt className='w-6 h-6' />
</Button>
}
</CardFooter>
</Card> </Card>
) )
} }
function createAgent() {
//just show a dialog for now similar to the agent card when the text is pressed
}
export default function Agents() { export default function Agents() {
const { data, error } = useSWR<AgentData[]>('agents', agentsFetcher, { revalidateOnFocus: false }); const { data, error } = useSWR<AgentData[]>('agents', agentsFetcher, { revalidateOnFocus: false });
const authenticatedData = useAuthenticatedData(); const authenticatedData = useAuthenticatedData();
@@ -318,42 +273,79 @@ export default function Agents() {
} }
return ( return (
<main className={styles.main}> <main className={`${styles.main} w-full ml-auto mr-auto`}>
<h3 <div className="float-right w-fit h-fit">
className='text-xl py-4'> <NavMenu selected="Agents" />
Agents </div>
</h3>
{ {
showLoginPrompt && showLoginPrompt &&
<LoginPrompt <LoginPrompt
loginRedirectMessage="Sign in to start chatting with a specialized agent" loginRedirectMessage="Sign in to start chatting with a specialized agent"
onOpenChange={setShowLoginPrompt} /> onOpenChange={setShowLoginPrompt} />
} }
<div className={`${styles.chatLayout} w-full ml-auto mr-auto`}>
<Alert> <div className={`${styles.sidePanel} top-0`}>
<Info className="h-4 w-4" /> <SidePanel
<AlertTitle>How this works</AlertTitle> webSocketConnected={true}
<AlertDescription> conversationId={null}
You can use any of these specialized agents to tailor to tune your conversation to your needs. uploadedFiles={[]}
{ isMobileWidth={isMobileWidth}
!authenticatedData && />
<> </div>
<div className='mt-3' /> <div className={`ml-auto mr-auto ${isMobileWidth ? "w-11/12" : "w-1/2"} pt-10`}>
<Button onClick={() => setShowLoginPrompt(true)}> <div className="pt-8 flex">
<UserCircle className='w-4 h-4 mr-2' /> Sign In <h1 className="text-3xl relative top-2">Agents</h1>
<div className="ml-auto float-right">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
className={`bg-[hsl(var(--background))] rounded-xl border dark:border-neutral-700 shadow-sm h-14 hover:bg-stone-100 dark:hover:bg-neutral-900`}
onClick={() => createAgent()}
>
<Plus className='w-6 h-6' color='gray' />
<p className="text-black dark:text-white ml-2">
<strong>Create Agent</strong>
</p>
</Button> </Button>
</> </AlertDialogTrigger>
<AlertDialogContent>
} <AlertDialogHeader>
<div className='mt-3' /> <AlertDialogTitle>Custom Agents</AlertDialogTitle>
<strong>Coming Soon:</strong> Support for making your own agents. <AlertDialogDescription>
</AlertDescription> Custom Agents will be coming to Khoj soon!
</Alert> </AlertDialogDescription>
<div className={styles.agentList}> </AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction asChild>
<Button className="bg-stone-100 dark:bg-[hsl(var(--background))] text-neutral-500 dark:text-white hover:bg-stone-100 dark:hover:bg-neutral-900" onClick={() => { }}>
Close
</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div>
<Card className={`mt-8 mb-6 pt-1 pb-1 bg-stone-100 dark:bg-[hsl(var(--background))]`}>
<CardContent>
<CardDescription className="flex flex-rows">
<Lightning className='w-4 h-4 mr-2 relative top-3' weight="fill" color="#a068f5" />
<p className="relative top-3">
<strong className="text-black dark:text-white pr-2">How it works</strong>
Use any of these specialized agents to tune your conversation to your needs.
</p>
</CardDescription>
</CardContent>
</Card>
</div>
<div className={`${styles.agentList}`}>
{data.map(agent => ( {data.map(agent => (
<AgentCard key={agent.slug} data={agent} userProfile={authenticatedData} isMobileWidth={isMobileWidth} /> <AgentCard key={agent.slug} data={agent} userProfile={authenticatedData} isMobileWidth={isMobileWidth} />
))} ))}
</div> </div>
</div>
</div>
</main> </main>
); );
} }

View File

@@ -700,6 +700,10 @@ export default function SidePanel(props: SidePanelProps) {
} }
}, [chatSessions]); }, [chatSessions]);
function newConvo() {
window.location.href = '/';
}
return ( return (
<div className={`${styles.panel} ${enabled ? styles.expanded : styles.collapsed}`}> <div className={`${styles.panel} ${enabled ? styles.expanded : styles.collapsed}`}>
<div className={`flex items-center justify-between ${(enabled || props.isMobileWidth) ? 'flex-row' : 'flex-col'}`}> <div className={`flex items-center justify-between ${(enabled || props.isMobileWidth) ? 'flex-row' : 'flex-col'}`}>

View File

@@ -8,8 +8,6 @@ import {
} from "@/components/ui/card" } from "@/components/ui/card"
import styles from "./suggestions.module.css"; import styles from "./suggestions.module.css";
import { getIconFromIconName } from "@/app/common/iconUtils"; import { getIconFromIconName } from "@/app/common/iconUtils";

View File

@@ -2,6 +2,10 @@ import type { Config } from "tailwindcss"
const config = { const config = {
safelist: [ safelist: [
{
pattern: /to-(blue|yellow|green|pink|purple|orange|red)-(50|100|200|950)/,
variants: ['dark'],
},
], ],
darkMode: ["class"], darkMode: ["class"],
content: [ content: [

File diff suppressed because it is too large Load Diff