mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-12-02 20:03:01 +01: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:
parent
8c12a69570
commit
3b7a9358c3
22 changed files with 1900 additions and 24 deletions
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>
|
||||||
|
);
|
||||||
|
}
|
86
src/interface/web/app/chat/chat.module.css
Normal file
86
src/interface/web/app/chat/chat.module.css
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
div.main {
|
||||||
|
height: 100vh;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.inputBox {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.inputBox {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.inputBox:focus {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBodyFull {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.inputBox {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: linear-gradient(var(--calm-green), var(--calm-blue));
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBody {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBox {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatLayout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBox {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.titleBar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
div.chatBody {
|
||||||
|
grid-template-columns: 0fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBox {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
24
src/interface/web/app/chat/layout.tsx
Normal file
24
src/interface/web/app/chat/layout.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Noto_Sans } from "next/font/google";
|
||||||
|
import "../globals.css";
|
||||||
|
|
||||||
|
const inter = Noto_Sans({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Khoj AI - Chat",
|
||||||
|
description: "Use this page to chat with Khoj AI.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className={inter.className}>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
106
src/interface/web/app/chat/page.tsx
Normal file
106
src/interface/web/app/chat/page.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import styles from './chat.module.css';
|
||||||
|
import React, { Suspense, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import SuggestionCard from '../components/suggestions/suggestionCard';
|
||||||
|
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
||||||
|
import ChatHistory from '../components/chatHistory/chatHistory';
|
||||||
|
import { SingleChatMessage } from '../components/chatMessage/chatMessage';
|
||||||
|
import NavMenu from '../components/navMenu/navMenu';
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
import ReferencePanel, { hasValidReferences } from '../components/referencePanel/referencePanel';
|
||||||
|
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
|
interface ChatOptions {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple'];
|
||||||
|
|
||||||
|
|
||||||
|
function ChatBodyData({ chatOptionsData }: { chatOptionsData: ChatOptions | null }) {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const conversationId = searchParams.get('conversationId');
|
||||||
|
const [showReferencePanel, setShowReferencePanel] = useState(true);
|
||||||
|
const [referencePanelData, setReferencePanelData] = useState<SingleChatMessage | null>(null);
|
||||||
|
|
||||||
|
if (!conversationId) {
|
||||||
|
return (
|
||||||
|
<div className={styles.suggestions}>
|
||||||
|
{chatOptionsData && Object.entries(chatOptionsData).map(([key, value]) => (
|
||||||
|
<SuggestionCard
|
||||||
|
key={key}
|
||||||
|
title={`/${key}`}
|
||||||
|
body={value}
|
||||||
|
link='#' // replace with actual link if available
|
||||||
|
styleClass={styleClassOptions[Math.floor(Math.random() * styleClassOptions.length)]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div className={(hasValidReferences(referencePanelData) && showReferencePanel) ? styles.chatBody : styles.chatBodyFull}>
|
||||||
|
<ChatHistory conversationId={conversationId} setReferencePanelData={setReferencePanelData} setShowReferencePanel={setShowReferencePanel} />
|
||||||
|
{
|
||||||
|
(hasValidReferences(referencePanelData) && showReferencePanel) &&
|
||||||
|
<ReferencePanel referencePanelData={referencePanelData} setShowReferencePanel={setShowReferencePanel} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Loading() {
|
||||||
|
return <h2>🌀 Loading...</h2>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChatInput(e: React.FormEvent<HTMLInputElement>) {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Chat() {
|
||||||
|
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
|
||||||
|
const [isLoading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/chat/options')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then((data: ChatOptions) => {
|
||||||
|
setLoading(false);
|
||||||
|
// Render chat options, if any
|
||||||
|
if (data) {
|
||||||
|
setChatOptionsData(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.main + " " + styles.chatLayout}>
|
||||||
|
<div className={styles.sidePanel}>
|
||||||
|
<SidePanel />
|
||||||
|
</div>
|
||||||
|
<div className={styles.chatBox}>
|
||||||
|
<title>
|
||||||
|
Khoj AI - Chat
|
||||||
|
</title>
|
||||||
|
<NavMenu selected="Chat" />
|
||||||
|
<div>
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<ChatBodyData chatOptionsData={chatOptionsData} />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
<div className={styles.inputBox}>
|
||||||
|
<input className={styles.inputBox} type="text" placeholder="Type here..." onInput={(e) => handleChatInput(e)} />
|
||||||
|
<button className={styles.inputBox}>Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
23
src/interface/web/app/common/auth.ts
Normal file
23
src/interface/web/app/common/auth.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import useSWR from 'swr'
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
photo: string;
|
||||||
|
is_active: boolean;
|
||||||
|
has_documents: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userFetcher = () => window.fetch('/api/v1/user').then(res => res.json()).catch(err => console.log(err));
|
||||||
|
|
||||||
|
export function useAuthenticatedData() {
|
||||||
|
|
||||||
|
const { data, error } = useSWR<UserProfile>('/api/v1/user', userFetcher, { revalidateOnFocus: false });
|
||||||
|
|
||||||
|
if (error) return null;
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
0
src/interface/web/app/common/chatFunctions.ts
Normal file
0
src/interface/web/app/common/chatFunctions.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
div.chatHistory {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatLayout {
|
||||||
|
height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
/* width: 80%; */
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
99
src/interface/web/app/components/chatHistory/chatHistory.tsx
Normal file
99
src/interface/web/app/components/chatHistory/chatHistory.tsx
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import styles from './chatHistory.module.css';
|
||||||
|
import { useRef, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import ChatMessage, { ChatHistoryData, SingleChatMessage } from '../chatMessage/chatMessage';
|
||||||
|
|
||||||
|
import renderMathInElement from 'katex/contrib/auto-render';
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
import 'highlight.js/styles/github.css'
|
||||||
|
|
||||||
|
interface ChatResponse {
|
||||||
|
status: string;
|
||||||
|
response: ChatHistoryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatHistory {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatHistoryProps {
|
||||||
|
conversationId: string;
|
||||||
|
setReferencePanelData: Function;
|
||||||
|
setShowReferencePanel: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function ChatHistory(props: ChatHistoryProps) {
|
||||||
|
const [data, setData] = useState<ChatHistoryData | null>(null);
|
||||||
|
const [isLoading, setLoading] = useState(true)
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const chatHistoryRef = useRef(null);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
fetch(`/api/chat/history?client=web&conversation_id=${props.conversationId}&n=10`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then((chatData: ChatResponse) => {
|
||||||
|
setLoading(false);
|
||||||
|
// Render chat options, if any
|
||||||
|
if (chatData) {
|
||||||
|
setData(chatData.response);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}, [props.conversationId]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new MutationObserver((mutationsList, observer) => {
|
||||||
|
// If the addedNodes property has one or more nodes
|
||||||
|
for(let mutation of mutationsList) {
|
||||||
|
if(mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
||||||
|
// Call your function here
|
||||||
|
renderMathInElement(document.body, {
|
||||||
|
delimiters: [
|
||||||
|
{ left: '$$', right: '$$', display: true },
|
||||||
|
{ left: '\\[', right: '\\]', display: true },
|
||||||
|
{ left: '$', right: '$', display: false },
|
||||||
|
{ left: '\\(', right: '\\)', display: false },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (chatHistoryRef.current) {
|
||||||
|
observer.observe(chatHistoryRef.current, { childList: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the observer on component unmount
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <h2>🌀 Loading...</h2>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.main + " " + styles.chatLayout}>
|
||||||
|
<div ref={ref}>
|
||||||
|
<div className={styles.chatHistory} ref={chatHistoryRef}>
|
||||||
|
{(data && data.chat) && data.chat.map((chatMessage, index) => (
|
||||||
|
<ChatMessage
|
||||||
|
key={index}
|
||||||
|
chatMessage={chatMessage}
|
||||||
|
setReferencePanelData={props.setReferencePanelData}
|
||||||
|
setShowReferencePanel={props.setShowReferencePanel}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
div.chatMessageContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid black;
|
||||||
|
/* max-width: 80%; */
|
||||||
|
}
|
||||||
|
|
||||||
|
div.you {
|
||||||
|
color: var(--frosted-background-color);
|
||||||
|
background-color: var(--intense-green);
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.khoj {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #000000;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatMessageContainer img {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatMessageContainer h3 img {
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.you .author {
|
||||||
|
color: var(--frosted-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.author {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #808080;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatFooter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatButtons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatFooter button {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--calm-blue);
|
||||||
|
color: var(--main-text-color);
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatFooter button:hover {
|
||||||
|
background-color: var(--frosted-background-color);
|
||||||
|
color: var(--intense-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatTimestamp {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.codeCopyButton {
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.codeCopyButton:hover {
|
||||||
|
background-color: var(--intense-green);
|
||||||
|
color: var(--frosted-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.feedbackButtons img,
|
||||||
|
button.copyButton img {
|
||||||
|
width: auto;
|
||||||
|
}
|
258
src/interface/web/app/components/chatMessage/chatMessage.tsx
Normal file
258
src/interface/web/app/components/chatMessage/chatMessage.tsx
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import styles from './chatMessage.module.css';
|
||||||
|
|
||||||
|
import markdownIt from 'markdown-it';
|
||||||
|
import mditHljs from "markdown-it-highlightjs";
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
import 'highlight.js/styles/github.css'
|
||||||
|
|
||||||
|
import { hasValidReferences } from '../referencePanel/referencePanel';
|
||||||
|
|
||||||
|
const md = new markdownIt({
|
||||||
|
html: true,
|
||||||
|
linkify: true,
|
||||||
|
typographer: true
|
||||||
|
});
|
||||||
|
|
||||||
|
md.use(mditHljs, {
|
||||||
|
inline: true,
|
||||||
|
code: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface Context {
|
||||||
|
compiled: string;
|
||||||
|
file: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebPage {
|
||||||
|
link: string;
|
||||||
|
query: string;
|
||||||
|
snippet: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrganicContext {
|
||||||
|
snippet: string;
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PeopleAlsoAsk {
|
||||||
|
link: string;
|
||||||
|
question: string;
|
||||||
|
snippet: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnlineContextData {
|
||||||
|
webpages: WebPage[];
|
||||||
|
answerBox: {
|
||||||
|
answer: string;
|
||||||
|
source: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
knowledgeGraph: {
|
||||||
|
attributes: {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
description: string;
|
||||||
|
descriptionLink: string;
|
||||||
|
descriptionSource: string;
|
||||||
|
imageUrl: string;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
organic: OrganicContext[];
|
||||||
|
peopleAlsoAsk: PeopleAlsoAsk[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentData {
|
||||||
|
name: string;
|
||||||
|
avatar: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Intent {
|
||||||
|
type: string;
|
||||||
|
"inferred-queries": string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SingleChatMessage {
|
||||||
|
automationId: string;
|
||||||
|
by: string;
|
||||||
|
intent: {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
message: string;
|
||||||
|
context: Context[];
|
||||||
|
created: string;
|
||||||
|
onlineContext: {
|
||||||
|
[key: string]: OnlineContextData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatHistoryData {
|
||||||
|
chat: SingleChatMessage[];
|
||||||
|
agent: AgentData;
|
||||||
|
conversation_id: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FeedbackButtons() {
|
||||||
|
return (
|
||||||
|
<div className={styles.feedbackButtons}>
|
||||||
|
<button className={styles.thumbsUpButton}>
|
||||||
|
<Image
|
||||||
|
src="/thumbs-up.svg"
|
||||||
|
alt="Thumbs Up"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button className={styles.thumbsDownButton}>
|
||||||
|
<Image
|
||||||
|
src="/thumbs-down.svg"
|
||||||
|
alt="Thumbs Down"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickMessage(event: React.MouseEvent<any>, chatMessage: SingleChatMessage, setReferencePanelData: Function, setShowReferencePanel: Function) {
|
||||||
|
event.preventDefault();
|
||||||
|
setReferencePanelData(chatMessage);
|
||||||
|
setShowReferencePanel(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatMessageProps {
|
||||||
|
chatMessage: SingleChatMessage;
|
||||||
|
setReferencePanelData: Function;
|
||||||
|
setShowReferencePanel: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatMessage(props: ChatMessageProps) {
|
||||||
|
const [copySuccess, setCopySuccess] = useState<boolean>(false);
|
||||||
|
|
||||||
|
let message = props.chatMessage.message;
|
||||||
|
|
||||||
|
// Replace LaTeX delimiters with placeholders
|
||||||
|
message = message.replace(/\\\(/g, 'LEFTPAREN').replace(/\\\)/g, 'RIGHTPAREN')
|
||||||
|
.replace(/\\\[/g, 'LEFTBRACKET').replace(/\\\]/g, 'RIGHTBRACKET');
|
||||||
|
|
||||||
|
if (props.chatMessage.intent && props.chatMessage.intent.type == "text-to-image2") {
|
||||||
|
message = `![generated_image](${message})\n\n${props.chatMessage.intent["inferred-queries"][0]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
let markdownRendered = md.render(message);
|
||||||
|
|
||||||
|
// Replace placeholders with LaTeX delimiters
|
||||||
|
markdownRendered = markdownRendered.replace(/LEFTPAREN/g, '\\(').replace(/RIGHTPAREN/g, '\\)')
|
||||||
|
.replace(/LEFTBRACKET/g, '\\[').replace(/RIGHTBRACKET/g, '\\]');
|
||||||
|
|
||||||
|
const messageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messageRef.current) {
|
||||||
|
const preElements = messageRef.current.querySelectorAll('pre > .hljs');
|
||||||
|
preElements.forEach((preElement) => {
|
||||||
|
const copyButton = document.createElement('button');
|
||||||
|
const copyImage = document.createElement('img');
|
||||||
|
copyImage.src = '/copy-button.svg';
|
||||||
|
copyImage.alt = 'Copy';
|
||||||
|
copyImage.width = 24;
|
||||||
|
copyImage.height = 24;
|
||||||
|
copyButton.appendChild(copyImage);
|
||||||
|
copyButton.className = `hljs ${styles.codeCopyButton}`
|
||||||
|
copyButton.addEventListener('click', () => {
|
||||||
|
let textContent = preElement.textContent || '';
|
||||||
|
// Strip any leading $ characters
|
||||||
|
textContent = textContent.replace(/^\$+/, '');
|
||||||
|
// Remove 'Copy' if it's at the start of the string
|
||||||
|
textContent = textContent.replace(/^Copy/, '');
|
||||||
|
textContent = textContent.trim();
|
||||||
|
navigator.clipboard.writeText(textContent);
|
||||||
|
});
|
||||||
|
preElement.prepend(copyButton);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [markdownRendered]);
|
||||||
|
|
||||||
|
function renderTimeStamp(timestamp: string) {
|
||||||
|
var dateObject = new Date(timestamp);
|
||||||
|
var month = dateObject.getMonth() + 1;
|
||||||
|
var date = dateObject.getDate();
|
||||||
|
var year = dateObject.getFullYear();
|
||||||
|
const formattedDate = `${month}/${date}/${year}`;
|
||||||
|
return `${formattedDate} ${dateObject.toLocaleTimeString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (copySuccess) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopySuccess(false);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}, [copySuccess]);
|
||||||
|
|
||||||
|
let referencesValid = hasValidReferences(props.chatMessage);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.chatMessageContainer} ${styles[props.chatMessage.by]}`}
|
||||||
|
onClick={props.chatMessage.by === "khoj" ? (event) => onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel) : undefined}>
|
||||||
|
{/* <div className={styles.chatFooter}> */}
|
||||||
|
{/* {props.chatMessage.by} */}
|
||||||
|
{/* </div> */}
|
||||||
|
<div ref={messageRef} className={styles.chatMessage} dangerouslySetInnerHTML={{ __html: markdownRendered }} />
|
||||||
|
{/* Add a copy button, thumbs up, and thumbs down buttons */}
|
||||||
|
<div className={styles.chatFooter}>
|
||||||
|
<div className={styles.chatTimestamp}>
|
||||||
|
{renderTimeStamp(props.chatMessage.created)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.chatButtons}>
|
||||||
|
{
|
||||||
|
referencesValid &&
|
||||||
|
<div className={styles.referenceButton}>
|
||||||
|
<button onClick={(event) => onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel)}>
|
||||||
|
References
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button className={`${styles.copyButton}`} onClick={() => {
|
||||||
|
navigator.clipboard.writeText(props.chatMessage.message);
|
||||||
|
setCopySuccess(true);
|
||||||
|
}}>
|
||||||
|
{
|
||||||
|
copySuccess ?
|
||||||
|
<Image
|
||||||
|
src="/copy-button-success.svg"
|
||||||
|
alt="Checkmark"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
: <Image
|
||||||
|
src="/copy-button.svg"
|
||||||
|
alt="Copy"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
{
|
||||||
|
props.chatMessage.by === "khoj" && <FeedbackButtons />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
96
src/interface/web/app/components/navMenu/navMenu.module.css
Normal file
96
src/interface/web/app/components/navMenu/navMenu.module.css
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
menu.menu a {
|
||||||
|
color: var(--main-text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: medium;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-self: center;
|
||||||
|
margin: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.menu a.selected {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.menu a:hover {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.menu {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.titleBar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
padding: 16px 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.titleBar menu {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.settingsMenuProfile img {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.settingsMenu {
|
||||||
|
color: var(--main-text-color);
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-self: center;
|
||||||
|
margin: 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.settingsMenu:hover {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.settingsMenuOptions {
|
||||||
|
display: block;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||||
|
top: 64px;
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.settingsMenuOptions a {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.settingsMenuUsername {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
menu.menu span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.settingsMenuOptions {
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
}
|
106
src/interface/web/app/components/navMenu/navMenu.tsx
Normal file
106
src/interface/web/app/components/navMenu/navMenu.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import styles from './navMenu.module.css';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useAuthenticatedData, UserProfile } from '@/app/common/auth';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
interface NavMenuProps {
|
||||||
|
selected: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsMenu(props: UserProfile) {
|
||||||
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.settingsMenu}>
|
||||||
|
<div className={styles.settingsMenuProfile} onClick={() => setShowSettings(!showSettings)}>
|
||||||
|
<Image
|
||||||
|
src={props.photo || "/agents.svg"}
|
||||||
|
alt={props.username}
|
||||||
|
width={50}
|
||||||
|
height={50}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{showSettings && (
|
||||||
|
<div className={styles.settingsMenuOptions}>
|
||||||
|
<div className={styles.settingsMenuUsername}>{props.username}</div>
|
||||||
|
<Link href="/config">
|
||||||
|
Settings
|
||||||
|
</Link>
|
||||||
|
<Link href="https://github.com/khoj-ai/khoj">
|
||||||
|
Github
|
||||||
|
</Link>
|
||||||
|
<Link href="https://docs.khoj.dev">
|
||||||
|
Help
|
||||||
|
</Link>
|
||||||
|
<Link href="/auth/logout">
|
||||||
|
Logout
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default function NavMenu(props: NavMenuProps) {
|
||||||
|
|
||||||
|
let userData = useAuthenticatedData();
|
||||||
|
return (
|
||||||
|
<div className={styles.titleBar}>
|
||||||
|
<Link href="/">
|
||||||
|
<Image
|
||||||
|
src="/khoj-logo.svg"
|
||||||
|
alt="Khoj Logo"
|
||||||
|
className={styles.logo}
|
||||||
|
width={100}
|
||||||
|
height={50}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<menu className={styles.menu}>
|
||||||
|
<a className={props.selected === "Chat" ? styles.selected : ""} href = '/chat'>
|
||||||
|
<Image
|
||||||
|
src="/chat.svg"
|
||||||
|
alt="Chat Logo"
|
||||||
|
className={styles.lgoo}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Chat
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a className={props.selected === "Agents" ? styles.selected : ""} href='/agents'>
|
||||||
|
<Image
|
||||||
|
src="/agents.svg"
|
||||||
|
alt="Agent Logo"
|
||||||
|
className={styles.lgoo}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Agents
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a className={props.selected === "Automations" ? styles.selected : ""} href = '/automations'>
|
||||||
|
<Image
|
||||||
|
src="/automation.svg"
|
||||||
|
alt="Automation Logo"
|
||||||
|
className={styles.lgoo}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Automations
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{userData && <SettingsMenu {...userData} />}
|
||||||
|
</menu>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
div.panel {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background-color: var(--calm-blue);
|
||||||
|
color: var(--main-text-color);
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.panel a {
|
||||||
|
color: var(--intense-green);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.onlineReference,
|
||||||
|
div.contextReference {
|
||||||
|
margin: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.contextReference:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.singleReference {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--frosted-background-color);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import styles from "./referencePanel.module.css";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import markdownIt from "markdown-it";
|
||||||
|
const md = new markdownIt({
|
||||||
|
html: true,
|
||||||
|
linkify: true,
|
||||||
|
typographer: true
|
||||||
|
});
|
||||||
|
|
||||||
|
import { SingleChatMessage, Context, WebPage, OnlineContextData } from "../chatMessage/chatMessage";
|
||||||
|
|
||||||
|
interface ReferencePanelProps {
|
||||||
|
referencePanelData: SingleChatMessage | null;
|
||||||
|
setShowReferencePanel: (showReferencePanel: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasValidReferences(referencePanelData: SingleChatMessage | null) {
|
||||||
|
return (
|
||||||
|
referencePanelData &&
|
||||||
|
(
|
||||||
|
(referencePanelData.context && referencePanelData.context.length > 0) ||
|
||||||
|
(referencePanelData.onlineContext && Object.keys(referencePanelData.onlineContext).length > 0 &&
|
||||||
|
Object.values(referencePanelData.onlineContext).some(
|
||||||
|
(onlineContextData) =>
|
||||||
|
(onlineContextData.webpages && onlineContextData.webpages.length > 0)|| onlineContextData.answerBox || onlineContextData.peopleAlsoAsk || onlineContextData.knowledgeGraph))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CompiledReference(props: { context: (Context | string) }) {
|
||||||
|
|
||||||
|
let snippet = "";
|
||||||
|
let file = "";
|
||||||
|
if (typeof props.context === "string") {
|
||||||
|
// Treat context as a string and get the first line for the file name
|
||||||
|
const lines = props.context.split("\n");
|
||||||
|
file = lines[0];
|
||||||
|
snippet = lines.slice(1).join("\n");
|
||||||
|
} else {
|
||||||
|
const context = props.context as Context;
|
||||||
|
snippet = context.compiled;
|
||||||
|
file = context.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [showSnippet, setShowSnippet] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.singleReference}>
|
||||||
|
<div className={styles.contextReference} onClick={() => setShowSnippet(!showSnippet)}>
|
||||||
|
<div className={styles.referencePanelTitle}>
|
||||||
|
{file}
|
||||||
|
</div>
|
||||||
|
<div className={styles.referencePanelContent} style={{ display: showSnippet ? "block" : "none" }}>
|
||||||
|
<div>
|
||||||
|
{snippet}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function WebPageReference(props: { webpages: WebPage, query: string | null }) {
|
||||||
|
|
||||||
|
let snippet = md.render(props.webpages.snippet);
|
||||||
|
|
||||||
|
const [showSnippet, setShowSnippet] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.onlineReference} onClick={() => setShowSnippet(!showSnippet)}>
|
||||||
|
<div className={styles.onlineReferenceTitle}>
|
||||||
|
<a href={props.webpages.link} target="_blank" rel="noreferrer">
|
||||||
|
{
|
||||||
|
props.query ? (
|
||||||
|
<span>
|
||||||
|
{props.query}
|
||||||
|
</span>
|
||||||
|
) : <span>
|
||||||
|
{props.webpages.query}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className={styles.onlineReferenceContent} style={{ display: showSnippet ? "block" : "none" }}>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: snippet }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function OnlineReferences(props: { onlineContext: OnlineContextData, query: string}) {
|
||||||
|
|
||||||
|
const webpages = props.onlineContext.webpages;
|
||||||
|
const answerBox = props.onlineContext.answerBox;
|
||||||
|
const peopleAlsoAsk = props.onlineContext.peopleAlsoAsk;
|
||||||
|
const knowledgeGraph = props.onlineContext.knowledgeGraph;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.singleReference}>
|
||||||
|
{
|
||||||
|
webpages && (
|
||||||
|
!Array.isArray(webpages) ? (
|
||||||
|
<WebPageReference webpages={webpages} query={props.query} />
|
||||||
|
) : (
|
||||||
|
webpages.map((webpage, index) => {
|
||||||
|
return <WebPageReference webpages={webpage} key={index} query={null} />
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
answerBox && (
|
||||||
|
<div className={styles.onlineReference}>
|
||||||
|
<div className={styles.onlineReferenceTitle}>
|
||||||
|
{answerBox.title}
|
||||||
|
</div>
|
||||||
|
<div className={styles.onlineReferenceContent}>
|
||||||
|
<div>
|
||||||
|
{answerBox.answer}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
peopleAlsoAsk && peopleAlsoAsk.map((people, index) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.onlineReference} key={index}>
|
||||||
|
<div className={styles.onlineReferenceTitle}>
|
||||||
|
<a href={people.link} target="_blank" rel="noreferrer">
|
||||||
|
{people.question}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className={styles.onlineReferenceContent}>
|
||||||
|
<div>
|
||||||
|
{people.snippet}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{
|
||||||
|
knowledgeGraph && (
|
||||||
|
<div className={styles.onlineReference}>
|
||||||
|
<div className={styles.onlineReferenceTitle}>
|
||||||
|
<a href={knowledgeGraph.descriptionLink} target="_blank" rel="noreferrer">
|
||||||
|
{knowledgeGraph.title}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className={styles.onlineReferenceContent}>
|
||||||
|
<div>
|
||||||
|
{knowledgeGraph.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReferencePanel(props: ReferencePanelProps) {
|
||||||
|
|
||||||
|
if (!props.referencePanelData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasValidReferences(props.referencePanelData)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.panel}`}>
|
||||||
|
References <button onClick={() => props.setShowReferencePanel(false)}>Hide</button>
|
||||||
|
{
|
||||||
|
props.referencePanelData?.context.map((context, index) => {
|
||||||
|
return <CompiledReference context={context} key={index} />
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Object.entries(props.referencePanelData?.onlineContext || {}).map(([key, onlineContextData], index) => {
|
||||||
|
return <OnlineReferences onlineContext={onlineContextData} query={key} key={index} />
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import styles from "./sidePanel.module.css";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { UserProfile } from "@/app/common/auth";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface ChatHistory {
|
||||||
|
conversation_id: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatSession(prop: ChatHistory) {
|
||||||
|
return (
|
||||||
|
<div key={prop.conversation_id} className={styles.session}>
|
||||||
|
<Link href={`/chat?conversationId=${prop.conversation_id}`}>
|
||||||
|
<p className={styles.session}>{prop.slug || "New Conversation 🌱"}</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatSessionsModalProps {
|
||||||
|
data: ChatHistory[];
|
||||||
|
setIsExpanded: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatSessionsModal({data, setIsExpanded}: ChatSessionsModalProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.modalSessionsList}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
{data.map((chatHistory) => (
|
||||||
|
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} />
|
||||||
|
))}
|
||||||
|
<button className={styles.showMoreButton} onClick={() => setIsExpanded(false)}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SidePanel() {
|
||||||
|
|
||||||
|
const [data, setData] = useState<ChatHistory[] | null>(null);
|
||||||
|
const [dataToShow, setDataToShow] = useState<ChatHistory[] | null>(null);
|
||||||
|
const [isLoading, setLoading] = useState(true)
|
||||||
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
fetch('/api/chat/sessions', { method: 'GET' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then((data: ChatHistory[]) => {
|
||||||
|
setLoading(false);
|
||||||
|
// Render chat options, if any
|
||||||
|
if (data) {
|
||||||
|
setData(data);
|
||||||
|
setDataToShow(data.slice(0, 5));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch('/api/v1/user', { method: 'GET' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then((data: UserProfile) => {
|
||||||
|
setUserProfile(data);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.panel}`}>
|
||||||
|
{
|
||||||
|
enabled ?
|
||||||
|
<div>
|
||||||
|
<div className={`${styles.expanded}`}>
|
||||||
|
<div className={`${styles.profile}`}>
|
||||||
|
{ userProfile &&
|
||||||
|
<div className={styles.profile}>
|
||||||
|
<img
|
||||||
|
className={styles.profile}
|
||||||
|
src={userProfile.photo}
|
||||||
|
alt="profile"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
<p>{userProfile?.username}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<button className={styles.button} onClick={() => setEnabled(false)}>
|
||||||
|
{/* Push Close Icon */}
|
||||||
|
<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M8.70710678,12 L19.5,12 C19.7761424,12 20,12.2238576 20,12.5 C20,12.7761424 19.7761424,13 19.5,13 L8.70710678,13 L11.8535534,16.1464466 C12.0488155,16.3417088 12.0488155,16.6582912 11.8535534,16.8535534 C11.6582912,17.0488155 11.3417088,17.0488155 11.1464466,16.8535534 L7.14644661,12.8535534 C6.95118446,12.6582912 6.95118446,12.3417088 7.14644661,12.1464466 L11.1464466,8.14644661 C11.3417088,7.95118446 11.6582912,7.95118446 11.8535534,8.14644661 C12.0488155,8.34170876 12.0488155,8.65829124 11.8535534,8.85355339 L8.70710678,12 L8.70710678,12 Z M4,5.5 C4,5.22385763 4.22385763,5 4.5,5 C4.77614237,5 5,5.22385763 5,5.5 L5,19.5 C5,19.7761424 4.77614237,20 4.5,20 C4.22385763,20 4,19.7761424 4,19.5 L4,5.5 Z"></path> </g></svg>
|
||||||
|
</button>
|
||||||
|
<h3>Recent Conversations</h3>
|
||||||
|
</div>
|
||||||
|
<div className={styles.sessionsList}>
|
||||||
|
{dataToShow && dataToShow.map((chatHistory) => (
|
||||||
|
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
(data && data.length > 5) && (
|
||||||
|
(isExpanded) ?
|
||||||
|
<ChatSessionsModal data={data} setIsExpanded={setIsExpanded} />
|
||||||
|
:
|
||||||
|
<button className={styles.showMoreButton} onClick={() => {
|
||||||
|
setIsExpanded(true);
|
||||||
|
}}>
|
||||||
|
Show All
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<div>
|
||||||
|
<div className={`${styles.collapsed}`}>
|
||||||
|
{ userProfile &&
|
||||||
|
<div className={`${styles.profile}`}>
|
||||||
|
<img
|
||||||
|
className={styles.profile}
|
||||||
|
src={userProfile.photo}
|
||||||
|
alt="profile"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button className={styles.button} onClick={() => setEnabled(true)}>
|
||||||
|
{/* Pull Open Icon */}
|
||||||
|
<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M15.2928932,12 L12.1464466,8.85355339 C11.9511845,8.65829124 11.9511845,8.34170876 12.1464466,8.14644661 C12.3417088,7.95118446 12.6582912,7.95118446 12.8535534,8.14644661 L16.8535534,12.1464466 C17.0488155,12.3417088 17.0488155,12.6582912 16.8535534,12.8535534 L12.8535534,16.8535534 C12.6582912,17.0488155 12.3417088,17.0488155 12.1464466,16.8535534 C11.9511845,16.6582912 11.9511845,16.3417088 12.1464466,16.1464466 L15.2928932,13 L4.5,13 C4.22385763,13 4,12.7761424 4,12.5 C4,12.2238576 4.22385763,12 4.5,12 L15.2928932,12 Z M19,5.5 C19,5.22385763 19.2238576,5 19.5,5 C19.7761424,5 20,5.22385763 20,5.5 L20,19.5 C20,19.7761424 19.7761424,20 19.5,20 C19.2238576,20 19,19.7761424 19,19.5 L19,5.5 Z"></path> </g></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
div.session {
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
max-width: 14rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.button {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.showMoreButton {
|
||||||
|
background: var(--intense-green);
|
||||||
|
border: none;
|
||||||
|
color: var(--frosted-background-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.panel {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background-color: var(--calm-blue);
|
||||||
|
color: var(--main-text-color);
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.expanded {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.collapsed {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.session:hover {
|
||||||
|
background-color: var(--calmer-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.session {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.profile {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
div.modalSessionsList {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--frosted-background-color);
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modalSessionsList div.content {
|
||||||
|
max-width: 80%;
|
||||||
|
max-height: 80%;
|
||||||
|
background-color: var(--frosted-background-color);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.modalSessionsList div.session {
|
||||||
|
max-width: 100%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import styles from "./suggestions.module.css";
|
||||||
|
|
||||||
|
interface SuggestionCardProps {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
link: string;
|
||||||
|
styleClass: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SuggestionCard(data: SuggestionCardProps) {
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
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;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
|
@ -148,33 +148,10 @@ def login_page(request: Request):
|
||||||
|
|
||||||
@web_client.get("/agents", response_class=HTMLResponse)
|
@web_client.get("/agents", response_class=HTMLResponse)
|
||||||
def agents_page(request: Request):
|
def agents_page(request: Request):
|
||||||
user: KhojUser = request.user.object if request.user.is_authenticated else None
|
|
||||||
user_picture = request.session.get("user", {}).get("picture") if user else None
|
|
||||||
has_documents = EntryAdapters.user_has_entries(user=user)
|
|
||||||
agents = AgentAdapters.get_all_accessible_agents(user)
|
|
||||||
agents_packet = list()
|
|
||||||
for agent in agents:
|
|
||||||
agents_packet.append(
|
|
||||||
{
|
|
||||||
"slug": agent.slug,
|
|
||||||
"avatar": agent.avatar,
|
|
||||||
"name": agent.name,
|
|
||||||
"personality": agent.personality,
|
|
||||||
"public": agent.public,
|
|
||||||
"creator": agent.creator.username if agent.creator else None,
|
|
||||||
"managed_by_admin": agent.managed_by_admin,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"agents.html",
|
"agents/index.html",
|
||||||
context={
|
context={
|
||||||
"request": request,
|
"request": request,
|
||||||
"agents": agents_packet,
|
|
||||||
"khoj_version": state.khoj_version,
|
|
||||||
"username": user.username if user else None,
|
|
||||||
"has_documents": has_documents,
|
|
||||||
"is_active": has_required_scope(request, ["premium"]),
|
|
||||||
"user_photo": user_picture,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue