mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-27 17:35:07 +01:00
Update the agents page with new UX (#850)
- Use icons/colors for setting the styling of agents - Update automations page to use the shadcn cards: https://github.com/shadcn-ui/ui
This commit is contained in:
parent
1c6ed9bc6d
commit
c837f3779e
12 changed files with 395 additions and 284 deletions
|
@ -16,28 +16,7 @@ div.agentPersonality {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.agentInfo {
|
|
||||||
font-size: medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentInfo a,
|
|
||||||
div.agentInfo h2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agent img {
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agent a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div#agentsHeader {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.infoButton {
|
button.infoButton {
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -47,63 +26,6 @@ button.infoButton {
|
||||||
font-size: medium;
|
font-size: medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
div#agentsHeader a,
|
|
||||||
div.agentInfo button {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 4px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
div#agentsHeader a:hover,
|
|
||||||
div.agentInfo button:hover {
|
|
||||||
box-shadow: 0 0 10px var(--primary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agent {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr auto;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: linear-gradient(18.48deg,rgba(252, 213, 87, 0.25) 2.76%,rgba(197, 0, 0, 0) 17.23%),linear-gradient(200.6deg,rgba(244, 229, 68, 0.25) 4.13%,rgba(230, 26, 26, 0) 20.54%);
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentModal {
|
|
||||||
background: linear-gradient(18.48deg,rgba(252, 213, 87, 0.25) 2.76%,rgba(197, 0, 0, 0) 17.23%),linear-gradient(200.6deg,rgba(244, 229, 68, 0.25) 4.13%,rgba(230, 26, 26, 0) 20.54%);
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentModalContent button {
|
|
||||||
width: 100%;
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentModalHeader {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentAvatar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentModalContent p {
|
|
||||||
white-space: break-spaces;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentInfo {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentList {
|
div.agentList {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -115,53 +37,6 @@ div.agentList {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.newConvoButton {
|
|
||||||
width: 20px;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentModalContainer {
|
|
||||||
position: fixed; /* Changed from absolute to fixed */
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%; /* This ensures it covers the viewport height */
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba(1,1,1,0.5);
|
|
||||||
z-index: 1000; /* Ensure it's above other content */
|
|
||||||
overflow-y: auto; /* Allows scrolling within the modal if needed */
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentModal {
|
|
||||||
position: relative;
|
|
||||||
width: 50%;
|
|
||||||
margin: auto;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentModalActions {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentModalActions button {
|
|
||||||
padding: 8px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.agentModalActions button:hover {
|
|
||||||
box-shadow: 0 0 10px var(hsla(--background));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 700px) {
|
@media only screen and (max-width: 700px) {
|
||||||
div.agentList {
|
div.agentList {
|
||||||
|
@ -171,39 +46,4 @@ div.agentModalActions button:hover {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.agentModal {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
.loader {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
border-top: 4px solid var(--primary-color);
|
|
||||||
border-right: 4px solid transparent;
|
|
||||||
box-sizing: border-box;
|
|
||||||
animation: rotation 1s linear infinite;
|
|
||||||
}
|
|
||||||
.loader::after {
|
|
||||||
content: '';
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border-bottom: 4px solid transparent;
|
|
||||||
animation: rotation 0.5s linear infinite reverse;
|
|
||||||
}
|
|
||||||
@keyframes rotation {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import type { Metadata } from "next";
|
||||||
import NavMenu from '../components/navMenu/navMenu';
|
import NavMenu from '../components/navMenu/navMenu';
|
||||||
import styles from './agentsLayout.module.css';
|
import styles from './agentsLayout.module.css';
|
||||||
|
|
||||||
|
import "../globals.css";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Khoj AI - Agents",
|
title: "Khoj AI - Agents",
|
||||||
description: "Use Agents with Khoj AI for deeper, more personalized queries.",
|
description: "Use Agents with Khoj AI for deeper, more personalized queries.",
|
||||||
|
@ -18,7 +20,7 @@ export default function RootLayout({
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.agentsLayout}`}>
|
<div className={`${styles.agentsLayout}`}>
|
||||||
<NavMenu selected="Agents" />
|
<NavMenu selected="Agents" showLogo={true} />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,13 +10,56 @@ 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 {
|
||||||
|
Lightbulb,
|
||||||
|
Robot,
|
||||||
|
Aperture,
|
||||||
|
GraduationCap,
|
||||||
|
Jeep,
|
||||||
|
Island,
|
||||||
|
MathOperations,
|
||||||
|
Asclepius,
|
||||||
|
Couch,
|
||||||
|
Code,
|
||||||
|
Atom,
|
||||||
|
ClockCounterClockwise,
|
||||||
|
PaperPlaneTilt,
|
||||||
|
Info,
|
||||||
|
UserCircle
|
||||||
|
} 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 { 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`} />,
|
||||||
|
};
|
||||||
|
|
||||||
export interface AgentData {
|
export interface AgentData {
|
||||||
slug: string;
|
slug: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
name: string;
|
name: string;
|
||||||
personality: string;
|
personality: string;
|
||||||
|
color: string;
|
||||||
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openChat(slug: string, userData: UserProfile | null) {
|
async function openChat(slug: string, userData: UserProfile | null) {
|
||||||
|
@ -39,86 +82,68 @@ 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 AgentModalProps {
|
|
||||||
data: AgentData;
|
|
||||||
setShowModal: (show: boolean) => void;
|
|
||||||
userData: UserProfile | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AgentCardProps {
|
interface AgentCardProps {
|
||||||
data: AgentData;
|
data: AgentData;
|
||||||
userProfile: UserProfile | null;
|
userProfile: UserProfile | null;
|
||||||
|
isMobileWidth: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AgentModal(props: AgentModalProps) {
|
function getIconFromIconName(iconName: string, color: string = 'gray', width: string = 'w-8', height: string = 'h-8') {
|
||||||
const [copiedToClipboard, setCopiedToClipboard] = useState(false);
|
const icon = iconMap[iconName];
|
||||||
|
const colorName = color.toLowerCase();
|
||||||
|
const colorClass = convertColorToTextClass(colorName);
|
||||||
|
|
||||||
useEffect(() => {
|
return icon ? icon(colorClass, width, height) : null;
|
||||||
if (copiedToClipboard) {
|
|
||||||
setTimeout(() => setCopiedToClipboard(false), 3000);
|
|
||||||
}
|
}
|
||||||
}, [copiedToClipboard]);
|
|
||||||
|
|
||||||
return (
|
function convertColorToClass(color: string) {
|
||||||
<div className={styles.agentModalContainer}>
|
// We can't dyanmically generate the classes for tailwindcss, so we have to explicitly use the whole string.
|
||||||
<div className={styles.agentModal}>
|
// See models/__init__.py 's definition of the Agent model for the color choices.
|
||||||
<div className={styles.agentModalContent}>
|
if (color === 'red') return `bg-red-500 hover:bg-red-600`;
|
||||||
<div className={styles.agentModalHeader}>
|
if (color === 'yellow') return `bg-yellow-500 hover:bg-yellow-600`;
|
||||||
<div className={styles.agentAvatar}>
|
if (color === 'green') return `bg-green-500 hover:bg-green-600`;
|
||||||
<Image
|
if (color === 'blue') return `bg-blue-500 hover:bg-blue-600`;
|
||||||
src={props.data.avatar}
|
if (color === 'orange') return `bg-orange-500 hover:bg-orange-600`;
|
||||||
alt={props.data.name}
|
if (color === 'purple') return `bg-purple-500 hover:bg-purple-600`;
|
||||||
width={50}
|
if (color === 'pink') return `bg-pink-500 hover:bg-pink-600`;
|
||||||
height={50}
|
if (color === 'teal') return `bg-teal-500 hover:bg-teal-600`;
|
||||||
/>
|
if (color === 'cyan') return `bg-cyan-500 hover:bg-cyan-600`;
|
||||||
<h2>{props.data.name}</h2>
|
if (color === 'lime') return `bg-lime-500 hover:bg-lime-600`;
|
||||||
</div>
|
if (color === 'indigo') return `bg-indigo-500 hover:bg-indigo-600`;
|
||||||
<div className={styles.agentModalActions}>
|
if (color === 'fuschia') return `bg-fuschia-500 hover:bg-fuschia-600`;
|
||||||
<Button className='bg-transparent hover:bg-yellow-500' onClick={() => {
|
if (color === 'rose') return `bg-rose-500 hover:bg-rose-600`;
|
||||||
navigator.clipboard.writeText(`${window.location.host}/agents?agent=${props.data.slug}`);
|
if (color === 'sky') return `bg-sky-500 hover:bg-sky-600`;
|
||||||
setCopiedToClipboard(true);
|
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`;
|
||||||
copiedToClipboard ?
|
|
||||||
<Image
|
|
||||||
src="copy-button-success.svg"
|
|
||||||
alt="Copied"
|
|
||||||
width={24}
|
|
||||||
height={24} />
|
|
||||||
: <Image
|
|
||||||
src="share.svg"
|
|
||||||
alt="Copy Link"
|
|
||||||
width={24}
|
|
||||||
height={24} />
|
|
||||||
}
|
}
|
||||||
</Button>
|
|
||||||
<Button className='bg-transparent hover:bg-yellow-500' onClick={() => {
|
function convertColorToTextClass(color: string) {
|
||||||
props.setShowModal(false);
|
if (color === 'red') return `text-red-500`;
|
||||||
}}>
|
if (color === 'yellow') return `text-yellow-500`;
|
||||||
<Image
|
if (color === 'green') return `text-green-500`;
|
||||||
src="close.svg"
|
if (color === 'blue') return `text-blue-500`;
|
||||||
alt="Close"
|
if (color === 'orange') return `text-orange-500`;
|
||||||
width={24}
|
if (color === 'purple') return `text-purple-500`;
|
||||||
height={24} />
|
if (color === 'pink') return `text-pink-500`;
|
||||||
</Button>
|
if (color === 'teal') return `text-teal-500`;
|
||||||
</div>
|
if (color === 'cyan') return `text-cyan-500`;
|
||||||
</div>
|
if (color === 'lime') return `text-lime-500`;
|
||||||
<p>{props.data.personality}</p>
|
if (color === 'indigo') return `text-indigo-500`;
|
||||||
<div className={styles.agentInfo}>
|
if (color === 'fuschia') return `text-fuschia-500`;
|
||||||
<Button className='bg-yellow-400 hover:bg-yellow-500' onClick={() => openChat(props.data.slug, props.userData)}>
|
if (color === 'rose') return `text-rose-500`;
|
||||||
Chat
|
if (color === 'sky') return `text-sky-500`;
|
||||||
</Button>
|
if (color === 'amber') return `text-amber-500`;
|
||||||
</div>
|
if (color === 'emerald') return `text-emerald-500`;
|
||||||
</div>
|
return `text-gray-500`;
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AgentCard(props: AgentCardProps) {
|
function AgentCard(props: AgentCardProps) {
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
const agentSlug = searchParams.get('agent');
|
const agentSlug = searchParams.get('agent');
|
||||||
const [showModal, setShowModal] = useState(agentSlug === props.data.slug);
|
const [showModal, setShowModal] = useState(agentSlug === props.data.slug);
|
||||||
|
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||||
|
|
||||||
const userData = props.userProfile;
|
const userData = props.userProfile;
|
||||||
|
|
||||||
|
@ -126,52 +151,144 @@ 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);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.agent}>
|
<Card className='shadow-md bg-secondary rounded-lg hover:shadow-lg'>
|
||||||
{
|
{
|
||||||
showModal && <AgentModal data={props.data} setShowModal={setShowModal} userData={userData} />
|
showLoginPrompt &&
|
||||||
|
<LoginPrompt
|
||||||
|
loginRedirectMessage={`Sign in to start chatting with ${props.data.name}`}
|
||||||
|
onOpenChange={setShowLoginPrompt} />
|
||||||
}
|
}
|
||||||
<Link href={`/agent/${props.data.slug}`}>
|
<CardHeader>
|
||||||
<div className={styles.agentAvatar}>
|
<CardTitle>
|
||||||
<Image
|
{
|
||||||
|
!props.isMobileWidth ?
|
||||||
|
<Dialog
|
||||||
|
open={showModal}
|
||||||
|
onOpenChange={() => {
|
||||||
|
setShowModal(!showModal);
|
||||||
|
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
|
||||||
|
}}>
|
||||||
|
<DialogTrigger>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
{
|
||||||
|
getIconFromIconName(props.data.icon, props.data.color) || <Image
|
||||||
src={props.data.avatar}
|
src={props.data.avatar}
|
||||||
alt={props.data.name}
|
alt={props.data.name}
|
||||||
width={50}
|
width={50}
|
||||||
height={50}
|
height={50}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
{props.data.name}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</DialogTrigger>
|
||||||
<div className={styles.agentInfo}>
|
<DialogContent className='whitespace-pre-line'>
|
||||||
<button className={styles.infoButton} onClick={() => {
|
<DialogHeader>
|
||||||
setShowModal(true);
|
<div className='flex items-center'>
|
||||||
} }>
|
{
|
||||||
<h2>{props.data.name}</h2>
|
getIconFromIconName(props.data.icon, props.data.color) || <Image
|
||||||
</button>
|
src={props.data.avatar}
|
||||||
</div>
|
alt={props.data.name}
|
||||||
<div className={styles.agentInfo}>
|
width={32}
|
||||||
<Button
|
height={50}
|
||||||
className='bg-yellow-400 hover:bg-yellow-500'
|
|
||||||
onClick={() => openChat(props.data.slug, userData)}>
|
|
||||||
<Image
|
|
||||||
src="send.svg"
|
|
||||||
alt="Chat"
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
/>
|
/>
|
||||||
</Button>
|
}
|
||||||
|
{props.data.name}
|
||||||
</div>
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
{props.data.personality}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
className={`${stylingString}`}
|
||||||
|
onClick={() => {
|
||||||
|
openChat(props.data.slug, userData);
|
||||||
|
setShowModal(false);
|
||||||
|
}}>
|
||||||
|
Chat
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
:
|
||||||
|
<Drawer
|
||||||
|
open={showModal}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setShowModal(open);
|
||||||
|
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
|
||||||
|
}}>
|
||||||
|
<DrawerTrigger>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
{
|
||||||
|
getIconFromIconName(props.data.icon, props.data.color) || <Image
|
||||||
|
src={props.data.avatar}
|
||||||
|
alt={props.data.name}
|
||||||
|
width={50}
|
||||||
|
height={50}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{props.data.name}
|
||||||
|
</div>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent className='whitespace-pre-line p-2'>
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>{props.data.name}</DrawerTitle>
|
||||||
|
<DrawerDescription>Full Prompt</DrawerDescription>
|
||||||
|
</DrawerHeader>
|
||||||
|
{props.data.personality}
|
||||||
|
<DrawerFooter>
|
||||||
|
<DrawerClose>
|
||||||
|
Done
|
||||||
|
</DrawerClose>
|
||||||
|
</DrawerFooter>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
<div className={styles.agentPersonality}>
|
<div className={styles.agentPersonality}>
|
||||||
<button className={styles.infoButton} onClick={() => setShowModal(true)}>
|
<button className={styles.infoButton} onClick={() => setShowModal(true)}>
|
||||||
<p>{props.data.personality}</p>
|
<p>{props.data.personality}</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 userData = useAuthenticatedData();
|
const authenticatedData = useAuthenticatedData();
|
||||||
|
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||||
|
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
setIsMobileWidth(window.innerWidth < 768);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
setIsMobileWidth(window.innerWidth < 768);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
@ -193,7 +310,7 @@ export default function Agents() {
|
||||||
Talk to a Specialized Agent
|
Talk to a Specialized Agent
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.agentList}>
|
<div className={styles.agentList}>
|
||||||
Loading agents...
|
<InlineLoading /> booting up your agents
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
@ -201,12 +318,39 @@ export default function Agents() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
<div className={`${styles.titleBar} text-5xl`}>
|
<h3
|
||||||
Talk to a Specialized Agent
|
className='text-xl py-4'>
|
||||||
</div>
|
Agents
|
||||||
|
</h3>
|
||||||
|
{
|
||||||
|
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}>
|
<div className={styles.agentList}>
|
||||||
{data.map(agent => (
|
{data.map(agent => (
|
||||||
<AgentCard key={agent.slug} data={agent} userProfile={userData} />
|
<AgentCard key={agent.slug} data={agent} userProfile={authenticatedData} isMobileWidth={isMobileWidth} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import NavMenu from '../components/navMenu/navMenu';
|
import NavMenu from '../components/navMenu/navMenu';
|
||||||
import styles from './automationsLayout.module.css';
|
import styles from './automationsLayout.module.css';
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
|
||||||
|
import "../globals.css";
|
||||||
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Khoj AI - Automations",
|
title: "Khoj AI - Automations",
|
||||||
|
|
|
@ -40,6 +40,7 @@ export default function NavMenu(props: NavMenuProps) {
|
||||||
|
|
||||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||||
const [darkMode, setDarkMode] = useState(false);
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
|
const [initialLoadDone, setInitialLoadDone] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMobileWidth(window.innerWidth < 768);
|
setIsMobileWidth(window.innerWidth < 768);
|
||||||
|
@ -62,13 +63,26 @@ export default function NavMenu(props: NavMenuProps) {
|
||||||
if (localStorage.getItem('theme') === 'dark') {
|
if (localStorage.getItem('theme') === 'dark') {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
setDarkMode(true);
|
setDarkMode(true);
|
||||||
} else if (mq.matches) {
|
} else if (localStorage.getItem('theme') === 'light') {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
setDarkMode(false);
|
||||||
|
} else {
|
||||||
|
const mq = window.matchMedia(
|
||||||
|
"(prefers-color-scheme: dark)"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mq.matches) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
setDarkMode(true);
|
setDarkMode(true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInitialLoadDone(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!initialLoadDone) return;
|
||||||
toggleDarkMode(darkMode);
|
toggleDarkMode(darkMode);
|
||||||
}, [darkMode]);
|
}, [darkMode]);
|
||||||
|
|
||||||
|
@ -128,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'>
|
<Menubar className='items-top inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground'>
|
||||||
<MenubarMenu>
|
<MenubarMenu>
|
||||||
<Link href='/chat' target="_blank" rel="noreferrer" className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>
|
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>
|
||||||
<MenubarTrigger>Chat</MenubarTrigger>
|
<MenubarTrigger>Chat</MenubarTrigger>
|
||||||
</Link>
|
</Link>
|
||||||
</MenubarMenu>
|
</MenubarMenu>
|
||||||
<MenubarMenu>
|
<MenubarMenu>
|
||||||
<Link href='/agents' target="_blank" rel="noreferrer" className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>
|
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>
|
||||||
<MenubarTrigger>Agents</MenubarTrigger>
|
<MenubarTrigger>Agents</MenubarTrigger>
|
||||||
</Link>
|
</Link>
|
||||||
</MenubarMenu>
|
</MenubarMenu>
|
||||||
<MenubarMenu>
|
<MenubarMenu>
|
||||||
<Link href='/automations' target="_blank" rel="noreferrer" className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>
|
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>
|
||||||
<MenubarTrigger>Automations</MenubarTrigger>
|
<MenubarTrigger>Automations</MenubarTrigger>
|
||||||
</Link>
|
</Link>
|
||||||
</MenubarMenu>
|
</MenubarMenu>
|
||||||
|
|
|
@ -39,17 +39,22 @@
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/katex": "^0.16.7",
|
"@types/katex": "^0.16.7",
|
||||||
"@types/markdown-it": "^14.1.1",
|
"@types/markdown-it": "^14.1.1",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"cronstrue": "^2.50.0",
|
|
||||||
"dompurify": "^3.1.6",
|
"dompurify": "^3.1.6",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "14.2.3",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cronstrue": "^2.50.0",
|
||||||
"katex": "^0.16.10",
|
"katex": "^0.16.10",
|
||||||
"lucide-react": "^0.397.0",
|
"lucide-react": "^0.397.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"markdown-it-highlightjs": "^4.1.0",
|
"markdown-it-highlightjs": "^4.1.0",
|
||||||
"next": "14.2.3",
|
"next": "14.2.3",
|
||||||
|
"nodemon": "^3.1.3",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
@ -58,6 +63,7 @@
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
|
"typescript": "^5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^0.9.1",
|
"vaul": "^0.9.1",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
|
|
|
@ -1140,10 +1140,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
|
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
|
||||||
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
|
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
|
||||||
|
|
||||||
"@types/node@^20":
|
"@types/node@20.14.10":
|
||||||
version "20.14.2"
|
version "20.14.10"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a"
|
||||||
integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==
|
integrity sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~5.26.4"
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-07-13 16:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("database", "0052_alter_searchmodelconfig_bi_encoder_docs_encode_config_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="agent",
|
||||||
|
name="style_color",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("blue", "Blue"),
|
||||||
|
("green", "Green"),
|
||||||
|
("red", "Red"),
|
||||||
|
("yellow", "Yellow"),
|
||||||
|
("orange", "Orange"),
|
||||||
|
("purple", "Purple"),
|
||||||
|
("pink", "Pink"),
|
||||||
|
("teal", "Teal"),
|
||||||
|
("cyan", "Cyan"),
|
||||||
|
("lime", "Lime"),
|
||||||
|
("indigo", "Indigo"),
|
||||||
|
("fuschia", "Fuschia"),
|
||||||
|
("rose", "Rose"),
|
||||||
|
("sky", "Sky"),
|
||||||
|
("amber", "Amber"),
|
||||||
|
("emerald", "Emerald"),
|
||||||
|
],
|
||||||
|
default="blue",
|
||||||
|
max_length=200,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="agent",
|
||||||
|
name="style_icon",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("Lightbulb", "Lighbulb"),
|
||||||
|
("Health", "Health"),
|
||||||
|
("Robot", "Robot"),
|
||||||
|
("Aperture", "Aperture"),
|
||||||
|
("GraduationCap", "Graduation Cap"),
|
||||||
|
("Jeep", "Jeep"),
|
||||||
|
("Island", "Island"),
|
||||||
|
("MathOperations", "Math Operations"),
|
||||||
|
("Asclepius", "Asclepius"),
|
||||||
|
("Couch", "Couch"),
|
||||||
|
("Code", "Code"),
|
||||||
|
("Atom", "Atom"),
|
||||||
|
("ClockCounterClockwise", "Clock Counter Clockwise"),
|
||||||
|
],
|
||||||
|
default="Lightbulb",
|
||||||
|
max_length=200,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -103,6 +103,39 @@ class VoiceModelOption(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Agent(BaseModel):
|
class Agent(BaseModel):
|
||||||
|
class StyleColorTypes(models.TextChoices):
|
||||||
|
BLUE = "blue"
|
||||||
|
GREEN = "green"
|
||||||
|
RED = "red"
|
||||||
|
YELLOW = "yellow"
|
||||||
|
ORANGE = "orange"
|
||||||
|
PURPLE = "purple"
|
||||||
|
PINK = "pink"
|
||||||
|
TEAL = "teal"
|
||||||
|
CYAN = "cyan"
|
||||||
|
LIME = "lime"
|
||||||
|
INDIGO = "indigo"
|
||||||
|
FUSCHIA = "fuschia"
|
||||||
|
ROSE = "rose"
|
||||||
|
SKY = "sky"
|
||||||
|
AMBER = "amber"
|
||||||
|
EMERALD = "emerald"
|
||||||
|
|
||||||
|
class StyleIconTypes(models.TextChoices):
|
||||||
|
LIGHBULB = "Lightbulb"
|
||||||
|
HEALTH = "Health"
|
||||||
|
ROBOT = "Robot"
|
||||||
|
APERTURE = "Aperture"
|
||||||
|
GRADUATION_CAP = "GraduationCap"
|
||||||
|
JEEP = "Jeep"
|
||||||
|
ISLAND = "Island"
|
||||||
|
MATH_OPERATIONS = "MathOperations"
|
||||||
|
ASCLEPIUS = "Asclepius"
|
||||||
|
COUCH = "Couch"
|
||||||
|
CODE = "Code"
|
||||||
|
ATOM = "Atom"
|
||||||
|
CLOCK_COUNTER_CLOCKWISE = "ClockCounterClockwise"
|
||||||
|
|
||||||
creator = models.ForeignKey(
|
creator = models.ForeignKey(
|
||||||
KhojUser, on_delete=models.CASCADE, default=None, null=True, blank=True
|
KhojUser, on_delete=models.CASCADE, default=None, null=True, blank=True
|
||||||
) # Creator will only be null when the agents are managed by admin
|
) # Creator will only be null when the agents are managed by admin
|
||||||
|
@ -114,6 +147,8 @@ class Agent(BaseModel):
|
||||||
managed_by_admin = models.BooleanField(default=False)
|
managed_by_admin = models.BooleanField(default=False)
|
||||||
chat_model = models.ForeignKey(ChatModelOptions, on_delete=models.CASCADE)
|
chat_model = models.ForeignKey(ChatModelOptions, on_delete=models.CASCADE)
|
||||||
slug = models.CharField(max_length=200)
|
slug = models.CharField(max_length=200)
|
||||||
|
style_color = models.CharField(max_length=200, choices=StyleColorTypes.choices, default=StyleColorTypes.BLUE)
|
||||||
|
style_icon = models.CharField(max_length=200, choices=StyleIconTypes.choices, default=StyleIconTypes.LIGHBULB)
|
||||||
|
|
||||||
|
|
||||||
class ProcessLock(BaseModel):
|
class ProcessLock(BaseModel):
|
||||||
|
|
|
@ -34,6 +34,8 @@ async def all_agents(
|
||||||
"public": agent.public,
|
"public": agent.public,
|
||||||
"creator": agent.creator.username if agent.creator else None,
|
"creator": agent.creator.username if agent.creator else None,
|
||||||
"managed_by_admin": agent.managed_by_admin,
|
"managed_by_admin": agent.managed_by_admin,
|
||||||
|
"color": agent.style_color,
|
||||||
|
"icon": agent.style_icon,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -212,6 +212,8 @@ def chat_history(
|
||||||
"name": conversation.agent.name,
|
"name": conversation.agent.name,
|
||||||
"avatar": conversation.agent.avatar,
|
"avatar": conversation.agent.avatar,
|
||||||
"isCreator": conversation.agent.creator == user,
|
"isCreator": conversation.agent.creator == user,
|
||||||
|
"color": conversation.agent.style_color,
|
||||||
|
"icon": conversation.agent.style_icon,
|
||||||
"persona": conversation.agent.personality,
|
"persona": conversation.agent.personality,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,6 +269,8 @@ def get_shared_chat(
|
||||||
"name": conversation.agent.name,
|
"name": conversation.agent.name,
|
||||||
"avatar": conversation.agent.avatar,
|
"avatar": conversation.agent.avatar,
|
||||||
"isCreator": conversation.agent.creator == user,
|
"isCreator": conversation.agent.creator == user,
|
||||||
|
"color": conversation.agent.style_color,
|
||||||
|
"icon": conversation.agent.style_icon,
|
||||||
"persona": conversation.agent.personality,
|
"persona": conversation.agent.personality,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
yarn.lock
Normal file
2
yarn.lock
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
Loading…
Reference in a new issue