diff --git a/src/interface/web/app/agents/agents.module.css b/src/interface/web/app/agents/agents.module.css
new file mode 100644
index 00000000..a02f24d8
--- /dev/null
+++ b/src/interface/web/app/agents/agents.module.css
@@ -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);
+ }
+}
diff --git a/src/interface/web/app/agents/agentsLayout.module.css b/src/interface/web/app/agents/agentsLayout.module.css
new file mode 100644
index 00000000..fd80922e
--- /dev/null
+++ b/src/interface/web/app/agents/agentsLayout.module.css
@@ -0,0 +1,10 @@
+.agentsLayout {
+ max-width: 70vw;
+ margin: auto;
+}
+
+@media screen and (max-width: 700px) {
+ .agentsLayout {
+ max-width: 90vw;
+ }
+}
diff --git a/src/interface/web/app/agents/layout.tsx b/src/interface/web/app/agents/layout.tsx
new file mode 100644
index 00000000..7e8e3d59
--- /dev/null
+++ b/src/interface/web/app/agents/layout.tsx
@@ -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 (
+
+
+ {children}
+
+ );
+}
diff --git a/src/interface/web/app/agents/page.tsx b/src/interface/web/app/agents/page.tsx
new file mode 100644
index 00000000..14413907
--- /dev/null
+++ b/src/interface/web/app/agents/page.tsx
@@ -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 (
+
+
+
+
+
+
+
{props.data.name}
+
+
+
+
+
+
+
{props.data.personality}
+
+
+
+
+
+
+ );
+}
+
+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 (
+
+ {
+ showModal &&
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default function Agents() {
+ const { data, error } = useSWR('agents', agentsFetcher, { revalidateOnFocus: false });
+ const userData = useAuthenticatedData();
+
+ if (error) {
+ return (
+
+
+ Talk to a Specialized Agent
+
+
+ Error loading agents
+
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+
+ Talk to a Specialized Agent
+
+
+ Loading agents...
+
+
+ );
+ }
+
+ return (
+
+
+ Talk to a Specialized Agent
+
+
+ {data.map(agent => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/interface/web/app/chat/chat.module.css b/src/interface/web/app/chat/chat.module.css
new file mode 100644
index 00000000..56b15d30
--- /dev/null
+++ b/src/interface/web/app/chat/chat.module.css
@@ -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;
+ }
+}
diff --git a/src/interface/web/app/chat/layout.tsx b/src/interface/web/app/chat/layout.tsx
new file mode 100644
index 00000000..46161774
--- /dev/null
+++ b/src/interface/web/app/chat/layout.tsx
@@ -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 (
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/interface/web/app/chat/page.tsx b/src/interface/web/app/chat/page.tsx
new file mode 100644
index 00000000..dbc7fc13
--- /dev/null
+++ b/src/interface/web/app/chat/page.tsx
@@ -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(null);
+
+ if (!conversationId) {
+ return (
+
+ {chatOptionsData && Object.entries(chatOptionsData).map(([key, value]) => (
+
+ ))}
+
+ );
+ }
+
+ return(
+
+
+ {
+ (hasValidReferences(referencePanelData) && showReferencePanel) &&
+
+ }
+
+ );
+}
+
+function Loading() {
+ return 🌀 Loading...
;
+}
+
+function handleChatInput(e: React.FormEvent) {
+ const target = e.target as HTMLInputElement;
+}
+
+export default function Chat() {
+ const [chatOptionsData, setChatOptionsData] = useState(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 (
+
+
+
+
+
+
+ Khoj AI - Chat
+
+
+
+ }>
+
+
+
+
+ handleChatInput(e)} />
+
+
+
+
+ )
+}
diff --git a/src/interface/web/app/common/auth.ts b/src/interface/web/app/common/auth.ts
new file mode 100644
index 00000000..45ac540b
--- /dev/null
+++ b/src/interface/web/app/common/auth.ts
@@ -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('/api/v1/user', userFetcher, { revalidateOnFocus: false });
+
+ if (error) return null;
+ if (!data) return null;
+
+ return data;
+}
diff --git a/src/interface/web/app/common/chatFunctions.ts b/src/interface/web/app/common/chatFunctions.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/interface/web/app/components/chatHistory/chatHistory.module.css b/src/interface/web/app/components/chatHistory/chatHistory.module.css
new file mode 100644
index 00000000..4597ae77
--- /dev/null
+++ b/src/interface/web/app/components/chatHistory/chatHistory.module.css
@@ -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;
+}
diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx
new file mode 100644
index 00000000..225d4049
--- /dev/null
+++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx
@@ -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(null);
+ const [isLoading, setLoading] = useState(true)
+ const ref = useRef(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 🌀 Loading...
;
+ }
+
+ return (
+
+
+
+ {(data && data.chat) && data.chat.map((chatMessage, index) => (
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/interface/web/app/components/chatMessage/chatMessage.module.css b/src/interface/web/app/components/chatMessage/chatMessage.module.css
new file mode 100644
index 00000000..d9d28b6b
--- /dev/null
+++ b/src/interface/web/app/components/chatMessage/chatMessage.module.css
@@ -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;
+}
diff --git a/src/interface/web/app/components/chatMessage/chatMessage.tsx b/src/interface/web/app/components/chatMessage/chatMessage.tsx
new file mode 100644
index 00000000..450236be
--- /dev/null
+++ b/src/interface/web/app/components/chatMessage/chatMessage.tsx
@@ -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 (
+
+
+
+
+ )
+}
+
+function onClickMessage(event: React.MouseEvent, 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(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(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 (
+ onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel) : undefined}>
+ {/*
*/}
+ {/* {props.chatMessage.by} */}
+ {/*
*/}
+
+ {/* Add a copy button, thumbs up, and thumbs down buttons */}
+
+
+ {renderTimeStamp(props.chatMessage.created)}
+
+
+ {
+ referencesValid &&
+
+
+
+ }
+
+ {
+ props.chatMessage.by === "khoj" &&
+ }
+
+
+
+ )
+}
diff --git a/src/interface/web/app/components/navMenu/navMenu.module.css b/src/interface/web/app/components/navMenu/navMenu.module.css
new file mode 100644
index 00000000..fa28d358
--- /dev/null
+++ b/src/interface/web/app/components/navMenu/navMenu.module.css
@@ -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;
+ }
+}
diff --git a/src/interface/web/app/components/navMenu/navMenu.tsx b/src/interface/web/app/components/navMenu/navMenu.tsx
new file mode 100644
index 00000000..7d2522d3
--- /dev/null
+++ b/src/interface/web/app/components/navMenu/navMenu.tsx
@@ -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 (
+
+
setShowSettings(!showSettings)}>
+
+
+ {showSettings && (
+
+
{props.username}
+
+ Settings
+
+
+ Github
+
+
+ Help
+
+
+ Logout
+
+
+ )}
+
+ );
+}
+export default function NavMenu(props: NavMenuProps) {
+
+ let userData = useAuthenticatedData();
+ return (
+
+ )
+}
diff --git a/src/interface/web/app/components/referencePanel/referencePanel.module.css b/src/interface/web/app/components/referencePanel/referencePanel.module.css
new file mode 100644
index 00000000..54ae350d
--- /dev/null
+++ b/src/interface/web/app/components/referencePanel/referencePanel.module.css
@@ -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;
+}
diff --git a/src/interface/web/app/components/referencePanel/referencePanel.tsx b/src/interface/web/app/components/referencePanel/referencePanel.tsx
new file mode 100644
index 00000000..93ea439d
--- /dev/null
+++ b/src/interface/web/app/components/referencePanel/referencePanel.tsx
@@ -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 (
+
+
setShowSnippet(!showSnippet)}>
+
+ {file}
+
+
+
+
+ )
+}
+
+function WebPageReference(props: { webpages: WebPage, query: string | null }) {
+
+ let snippet = md.render(props.webpages.snippet);
+
+ const [showSnippet, setShowSnippet] = useState(false);
+
+ return (
+ setShowSnippet(!showSnippet)}>
+
+
+
+ )
+}
+
+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 (
+
+ {
+ webpages && (
+ !Array.isArray(webpages) ? (
+
+ ) : (
+ webpages.map((webpage, index) => {
+ return
+ })
+ )
+ )
+ }
+ {
+ answerBox && (
+
+
+ {answerBox.title}
+
+
+
+ {answerBox.answer}
+
+
+
+ )
+ }
+ {
+ peopleAlsoAsk && peopleAlsoAsk.map((people, index) => {
+ return (
+
+ )
+ })
+ }
+ {
+ knowledgeGraph && (
+
+
+
+
+ {knowledgeGraph.description}
+
+
+
+ )
+ }
+
+
+ )
+}
+
+export default function ReferencePanel(props: ReferencePanelProps) {
+
+ if (!props.referencePanelData) {
+ return null;
+ }
+
+ if (!hasValidReferences(props.referencePanelData)) {
+ return null;
+ }
+
+ return (
+
+ References
+ {
+ props.referencePanelData?.context.map((context, index) => {
+ return
+ })
+ }
+ {
+ Object.entries(props.referencePanelData?.onlineContext || {}).map(([key, onlineContextData], index) => {
+ return
+ })
+ }
+
+ );
+}
diff --git a/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx b/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx
new file mode 100644
index 00000000..a6561423
--- /dev/null
+++ b/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx
@@ -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 (
+
+
+
{prop.slug || "New Conversation 🌱"}
+
+
+ );
+}
+
+interface ChatSessionsModalProps {
+ data: ChatHistory[];
+ setIsExpanded: React.Dispatch>;
+}
+
+function ChatSessionsModal({data, setIsExpanded}: ChatSessionsModalProps) {
+ return (
+
+
+ {data.map((chatHistory) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default function SidePanel() {
+
+ const [data, setData] = useState(null);
+ const [dataToShow, setDataToShow] = useState(null);
+ const [isLoading, setLoading] = useState(true)
+ const [enabled, setEnabled] = useState(false);
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ const [userProfile, setUserProfile] = useState(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 (
+
+ {
+ enabled ?
+
+
+
+ { userProfile &&
+
+
+
{userProfile?.username}
+
+ }
+
+
+
Recent Conversations
+
+
+ {dataToShow && dataToShow.map((chatHistory) => (
+
+ ))}
+
+ {
+ (data && data.length > 5) && (
+ (isExpanded) ?
+
+ :
+
+ )
+ }
+
+ :
+
+
+ { userProfile &&
+
+
+
+ }
+
+
+
+ }
+
+
+ );
+}
diff --git a/src/interface/web/app/components/sidePanel/sidePanel.module.css b/src/interface/web/app/components/sidePanel/sidePanel.module.css
new file mode 100644
index 00000000..ea675fa0
--- /dev/null
+++ b/src/interface/web/app/components/sidePanel/sidePanel.module.css
@@ -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;
+}
diff --git a/src/interface/web/app/components/suggestions/suggestionCard.tsx b/src/interface/web/app/components/suggestions/suggestionCard.tsx
new file mode 100644
index 00000000..27ba7590
--- /dev/null
+++ b/src/interface/web/app/components/suggestions/suggestionCard.tsx
@@ -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 (
+
+
+ {data.title}
+
+
+ {data.body}
+
+
+
+ );
+}
diff --git a/src/interface/web/app/components/suggestions/suggestions.module.css b/src/interface/web/app/components/suggestions/suggestions.module.css
new file mode 100644
index 00000000..0663f7cf
--- /dev/null
+++ b/src/interface/web/app/components/suggestions/suggestions.module.css
@@ -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;
+}
diff --git a/src/khoj/routers/web_client.py b/src/khoj/routers/web_client.py
index 1dc6a2f4..318ab1a2 100644
--- a/src/khoj/routers/web_client.py
+++ b/src/khoj/routers/web_client.py
@@ -148,33 +148,10 @@ def login_page(request: Request):
@web_client.get("/agents", response_class=HTMLResponse)
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(
- "agents.html",
+ "agents/index.html",
context={
"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,
},
)