mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 21:29:08 +00:00
Add our first view via Next.js for Agents (#817)
Initialize our migration to use Next.js for front-end views via Agents. This includes setup for getting authenticated users, reading in available agents, setting up a pop-up modal when you're clicking on an agent, and allowing users to start new conversations with agents. Best attempt at an in-place migration, though there are some noticeable differences. Also adds view for chat that are not being used, but in experimental phase.
This commit is contained in:
215
src/interface/web/app/agents/agents.module.css
Normal file
215
src/interface/web/app/agents/agents.module.css
Normal file
@@ -0,0 +1,215 @@
|
||||
div.titleBar {
|
||||
padding: 16px 0;
|
||||
text-align: center;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.agentPersonality p {
|
||||
white-space: inherit;
|
||||
overflow: hidden;
|
||||
height: 78px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
div.agentPersonality {
|
||||
text-align: left;
|
||||
grid-column: span 3;
|
||||
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;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
div#agentsHeader {
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
|
||||
button.infoButton {
|
||||
border: none;
|
||||
background-color: transparent !important;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
div#agentsHeader a,
|
||||
div.agentInfo button {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: var(--summer-sun);
|
||||
font: inherit;
|
||||
color: var(--main-text-color);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
div#agentsHeader a:hover,
|
||||
div.agentInfo button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
box-shadow: 0 0 10px var(--primary-hover);
|
||||
}
|
||||
|
||||
div.agent {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: 20px;
|
||||
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 {
|
||||
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.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 {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
margin-right: auto;
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
svg.newConvoButton {
|
||||
width: 20px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
div.agentModalContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(1,1,1,0.5);
|
||||
}
|
||||
|
||||
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;
|
||||
background-color: var(--summer-sun);
|
||||
color: var(--main-text-color);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
div.agentModalActions button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
box-shadow: 0 0 10px var(--primary-hover);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
div.agentList {
|
||||
width: 90%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
}
|
||||
.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-left: 4px solid var(--summer-sun);
|
||||
border-bottom: 4px solid transparent;
|
||||
animation: rotation 0.5s linear infinite reverse;
|
||||
}
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
10
src/interface/web/app/agents/agentsLayout.module.css
Normal file
10
src/interface/web/app/agents/agentsLayout.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.agentsLayout {
|
||||
max-width: 70vw;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.agentsLayout {
|
||||
max-width: 90vw;
|
||||
}
|
||||
}
|
||||
25
src/interface/web/app/agents/layout.tsx
Normal file
25
src/interface/web/app/agents/layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import NavMenu from '../components/navMenu/navMenu';
|
||||
import styles from './agentsLayout.module.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI - Agents",
|
||||
description: "Use Agents with Khoj AI for deeper, more personalized queries.",
|
||||
icons: {
|
||||
icon: '/static/favicon.ico',
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<div className={`${styles.agentsLayout}`}>
|
||||
<NavMenu selected="Agents" />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
208
src/interface/web/app/agents/page.tsx
Normal file
208
src/interface/web/app/agents/page.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
'use client'
|
||||
|
||||
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';
|
||||
|
||||
import { useAuthenticatedData, UserProfile } from '../common/auth';
|
||||
|
||||
|
||||
export interface AgentData {
|
||||
slug: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
personality: string;
|
||||
}
|
||||
|
||||
async function openChat(slug: string, userData: UserProfile | null) {
|
||||
|
||||
const unauthenticatedRedirectUrl = `/login?next=/agents?agent=${slug}`;
|
||||
if (!userData) {
|
||||
window.location.href = unauthenticatedRedirectUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/chat/sessions?agent_slug=${slug}`, { method: "POST" });
|
||||
// const data = await response.json();
|
||||
if (response.status == 200) {
|
||||
window.location.href = `/chat`;
|
||||
} else if(response.status == 403 || response.status == 401) {
|
||||
window.location.href = unauthenticatedRedirectUrl;
|
||||
} else {
|
||||
alert("Failed to start chat session");
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
data: AgentData;
|
||||
userProfile: UserProfile | null;
|
||||
}
|
||||
|
||||
function AgentModal(props: AgentModalProps) {
|
||||
const [copiedToClipboard, setCopiedToClipboard] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (copiedToClipboard) {
|
||||
setTimeout(() => setCopiedToClipboard(false), 3000);
|
||||
}
|
||||
}, [copiedToClipboard]);
|
||||
|
||||
return (
|
||||
<div className={styles.agentModalContainer}>
|
||||
<div className={styles.agentModal}>
|
||||
<div className={styles.agentModalContent}>
|
||||
<div className={styles.agentModalHeader}>
|
||||
<div className={styles.agentAvatar}>
|
||||
<Image
|
||||
src={props.data.avatar}
|
||||
alt={props.data.name}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
<h2>{props.data.name}</h2>
|
||||
</div>
|
||||
<div className={styles.agentModalActions}>
|
||||
<button onClick={() => {
|
||||
navigator.clipboard.writeText(`${window.location.host}/agents?agent=${props.data.slug}`);
|
||||
setCopiedToClipboard(true);
|
||||
}}>
|
||||
{
|
||||
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 onClick={() => props.setShowModal(false)}>
|
||||
<Image
|
||||
src="Close.svg"
|
||||
alt="Close"
|
||||
width={24}
|
||||
height={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p>{props.data.personality}</p>
|
||||
<div className={styles.agentInfo}>
|
||||
<button onClick={() => openChat(props.data.slug, props.userData)}>
|
||||
Chat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentCard(props: AgentCardProps) {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const agentSlug = searchParams.get('agent');
|
||||
const [showModal, setShowModal] = useState(agentSlug === props.data.slug);
|
||||
|
||||
const userData = props.userProfile;
|
||||
|
||||
if (showModal) {
|
||||
window.history.pushState({}, `Khoj AI - Agent ${props.data.slug}`, `/agents?agent=${props.data.slug}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.agent}>
|
||||
{
|
||||
showModal && <AgentModal data={props.data} setShowModal={setShowModal} userData={userData} />
|
||||
}
|
||||
<Link href={`/agent/${props.data.slug}`}>
|
||||
<div className={styles.agentAvatar}>
|
||||
<Image
|
||||
src={props.data.avatar}
|
||||
alt={props.data.name}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<div className={styles.agentInfo}>
|
||||
<button className={styles.infoButton} onClick={() => setShowModal(true)}>
|
||||
<h2>{props.data.name}</h2>
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.agentInfo}>
|
||||
<button onClick={() => openChat(props.data.slug, userData)}>
|
||||
<Image
|
||||
src="send.svg"
|
||||
alt="Chat"
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.agentPersonality}>
|
||||
<button className={styles.infoButton} onClick={() => setShowModal(true)}>
|
||||
<p>{props.data.personality}</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Agents() {
|
||||
const { data, error } = useSWR<AgentData[]>('agents', agentsFetcher, { revalidateOnFocus: false });
|
||||
const userData = useAuthenticatedData();
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.titleBar}>
|
||||
Talk to a Specialized Agent
|
||||
</div>
|
||||
<div className={styles.agentList}>
|
||||
Error loading agents
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.titleBar}>
|
||||
Talk to a Specialized Agent
|
||||
</div>
|
||||
<div className={styles.agentList}>
|
||||
Loading agents...
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.titleBar}>
|
||||
Talk to a Specialized Agent
|
||||
</div>
|
||||
<div className={styles.agentList}>
|
||||
{data.map(agent => (
|
||||
<AgentCard key={agent.slug} data={agent} userProfile={userData} />
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user