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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1095 additions and 343 deletions

View file

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

View file

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

View file

@ -1,27 +1,34 @@
import type { Metadata } from "next";
import NavMenu from '../components/navMenu/navMenu';
import styles from './agentsLayout.module.css';
import { Noto_Sans } from "next/font/google";
import "../globals.css";
const inter = Noto_Sans({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Khoj AI - Agents",
description: "Use Agents with Khoj AI for deeper, more personalized queries.",
icons: {
icon: '/static/favicon.ico',
},
title: "Khoj AI - Chat",
description: "Use this page to chat with Khoj AI.",
};
export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<div className={`${styles.agentsLayout}`}>
<NavMenu selected="Agents" showLogo={true} />
{children}
</div>
);
return (
<html lang="en">
<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}
</body>
</html>
);
}

View file

@ -3,7 +3,6 @@
import styles from './agents.module.css';
import Image from 'next/image';
import Link from 'next/link';
import useSWR from 'swr';
import { useEffect, useState } from 'react';
@ -11,47 +10,31 @@ import { useEffect, useState } from 'react';
import { useAuthenticatedData, UserProfile } from '../common/auth';
import { Button } from '@/components/ui/button';
import {
Lightbulb,
Robot,
Aperture,
GraduationCap,
Jeep,
Island,
MathOperations,
Asclepius,
Couch,
Code,
Atom,
ClockCounterClockwise,
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import {
PaperPlaneTilt,
Info,
UserCircle
Lightning,
Plus,
} from "@phosphor-icons/react";
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 LoginPrompt from '../components/loginPrompt/loginPrompt';
import Loading, { InlineLoading } from '../components/loading/loading';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
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`} />,
};
import { InlineLoading } from '../components/loading/loading';
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
import NavMenu from '../components/navMenu/navMenu';
import { getIconFromIconName } from '../common/iconUtils';
import { convertColorToTextClass } from '../common/colorUtils';
export interface AgentData {
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));
interface AgentCardProps {
data: AgentData;
userProfile: UserProfile | null;
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) {
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}`);
}
const stylingString = convertColorToClass(props.data.color);
const stylingString = convertColorToTextClass(props.data.color);
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 &&
<LoginPrompt
@ -173,7 +106,7 @@ function AgentCard(props: AgentCardProps) {
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
}}>
<DialogTrigger>
<div className='flex items-center'>
<div className='flex items-center relative top-2'>
{
getIconFromIconName(props.data.icon, props.data.color) || <Image
src={props.data.avatar}
@ -185,7 +118,22 @@ function AgentCard(props: AgentCardProps) {
{props.data.name}
</div>
</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>
<div className='flex items-center'>
{
@ -196,18 +144,21 @@ function AgentCard(props: AgentCardProps) {
height={50}
/>
}
{props.data.name}
<p className="font-bold text-lg">{props.data.name}</p>
</div>
</DialogHeader>
{props.data.personality}
<div className="max-h-[60vh] overflow-y-scroll text-neutral-500 dark:text-white">
{props.data.personality}
</div>
<DialogFooter>
<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={() => {
openChat(props.data.slug, userData);
setShowModal(false);
}}>
Chat
<PaperPlaneTilt className='mr-2 w-6 h-6' color={props.data.color} />
Start Chatting
</Button>
</DialogFooter>
</DialogContent>
@ -232,6 +183,21 @@ function AgentCard(props: AgentCardProps) {
{props.data.name}
</div>
</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'>
<DrawerHeader>
<DrawerTitle>{props.data.name}</DrawerTitle>
@ -250,31 +216,20 @@ function AgentCard(props: AgentCardProps) {
</CardHeader>
<CardContent>
<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>
</button>
</div>
</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>
)
}
function createAgent() {
//just show a dialog for now similar to the agent card when the text is pressed
}
export default function Agents() {
const { data, error } = useSWR<AgentData[]>('agents', agentsFetcher, { revalidateOnFocus: false });
const authenticatedData = useAuthenticatedData();
@ -318,41 +273,78 @@ export default function Agents() {
}
return (
<main className={styles.main}>
<h3
className='text-xl py-4'>
Agents
</h3>
<main className={`${styles.main} w-full ml-auto mr-auto`}>
<div className="float-right w-fit h-fit">
<NavMenu selected="Agents" />
</div>
{
showLoginPrompt &&
<LoginPrompt
loginRedirectMessage="Sign in to start chatting with a specialized agent"
onOpenChange={setShowLoginPrompt} />
}
<Alert>
<Info className="h-4 w-4" />
<AlertTitle>How this works</AlertTitle>
<AlertDescription>
You can use any of these specialized agents to tailor to tune your conversation to your needs.
{
!authenticatedData &&
<>
<div className='mt-3' />
<Button onClick={() => setShowLoginPrompt(true)}>
<UserCircle className='w-4 h-4 mr-2' /> Sign In
</Button>
</>
}
<div className='mt-3' />
<strong>Coming Soon:</strong> Support for making your own agents.
</AlertDescription>
</Alert>
<div className={styles.agentList}>
{data.map(agent => (
<AgentCard key={agent.slug} data={agent} userProfile={authenticatedData} isMobileWidth={isMobileWidth} />
))}
<div className={`${styles.chatLayout} w-full ml-auto mr-auto`}>
<div className={`${styles.sidePanel} top-0`}>
<SidePanel
webSocketConnected={true}
conversationId={null}
uploadedFiles={[]}
isMobileWidth={isMobileWidth}
/>
</div>
<div className={`ml-auto mr-auto ${isMobileWidth ? "w-11/12" : "w-1/2"} pt-10`}>
<div className="pt-8 flex">
<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>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Custom Agents</AlertDialogTitle>
<AlertDialogDescription>
Custom Agents will be coming to Khoj soon!
</AlertDialogDescription>
</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 => (
<AgentCard key={agent.slug} data={agent} userProfile={authenticatedData} isMobileWidth={isMobileWidth} />
))}
</div>
</div>
</div>
</main>
);

View file

@ -700,6 +700,10 @@ export default function SidePanel(props: SidePanelProps) {
}
}, [chatSessions]);
function newConvo() {
window.location.href = '/';
}
return (
<div className={`${styles.panel} ${enabled ? styles.expanded : styles.collapsed}`}>
<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"
import styles from "./suggestions.module.css";
import { getIconFromIconName } from "@/app/common/iconUtils";

View file

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

File diff suppressed because it is too large Load diff