Checkpoint: Updated sidebar panel with new components

- Add non-functional UI elements for chat, references, feedback buttons, rename/share session, mic, attachment, websocket connection
This commit is contained in:
sabaimran
2024-07-02 11:18:50 +05:30
parent c83b8f2768
commit 541ce04ebc
27 changed files with 1874 additions and 385 deletions

View File

@@ -96,7 +96,7 @@ function AgentModal(props: AgentModalProps) {
props.setShowModal(false); props.setShowModal(false);
}}> }}>
<Image <Image
src="Close.svg" src="close.svg"
alt="Close" alt="Close"
width={24} width={24}
height={24} /> height={24} />

View File

@@ -12,23 +12,27 @@ div.main {
div.inputBox { div.inputBox {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: auto 1fr auto auto;
padding: 1rem; border: 1px solid var(--border-color);
border-radius: 1rem; border-radius: 16px;
background-color: #f5f5f5; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03);
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.1); margin-bottom: 20px;
gap: 12px;
padding-left: 20px;
padding-right: 20px;
} }
input.inputBox { input.inputBox {
border: none; border: none;
}
input.inputBox:focus {
outline: none; outline: none;
background-color: transparent; background-color: transparent;
} }
input.inputBox:focus { div.inputBox:focus {
border: none; box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
outline: none;
background-color: transparent;
} }
div.chatBodyFull { div.chatBodyFull {
@@ -65,9 +69,7 @@ div.chatLayout {
div.chatBox { div.chatBox {
display: grid; display: grid;
gap: 1rem;
height: 100%; height: 100%;
padding: 1rem;
} }
div.titleBar { div.titleBar {
@@ -75,6 +77,25 @@ div.titleBar {
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
} }
div.chatBoxBody {
display: grid;
height: 100%;
width: 70%;
margin: auto;
}
div.agentIndicator a {
display: flex;
text-align: center;
align-content: center;
align-items: center;
}
div.agentIndicator {
padding: 10px;
}
@media (max-width: 768px) { @media (max-width: 768px) {
div.chatBody { div.chatBody {
grid-template-columns: 0fr 1fr; grid-template-columns: 0fr 1fr;
@@ -84,3 +105,22 @@ div.titleBar {
padding: 0; padding: 0;
} }
} }
@media screen and (max-width: 768px) {
div.inputBox {
margin-bottom: 0px;
}
div.chatBoxBody {
width: 100%;
}
div.chatBox {
padding: 0;
}
div.chatLayout {
gap: 0;
}
}

View File

@@ -6,29 +6,52 @@ import React, { Suspense, useEffect, useState } from 'react';
import SuggestionCard from '../components/suggestions/suggestionCard'; import SuggestionCard from '../components/suggestions/suggestionCard';
import SidePanel from '../components/sidePanel/chatHistorySidePanel'; import SidePanel from '../components/sidePanel/chatHistorySidePanel';
import ChatHistory from '../components/chatHistory/chatHistory'; import ChatHistory from '../components/chatHistory/chatHistory';
import { SingleChatMessage } from '../components/chatMessage/chatMessage';
import NavMenu from '../components/navMenu/navMenu'; import NavMenu from '../components/navMenu/navMenu';
import { useSearchParams } from 'next/navigation' import { useSearchParams } from 'next/navigation'
import ReferencePanel, { hasValidReferences } from '../components/referencePanel/referencePanel'; import Loading from '../components/loading/loading';
import { setupWebSocket } from '../common/chatFunctions';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
import { Lightbulb, ArrowCircleUp, FileArrowUp, Microphone } from '@phosphor-icons/react';
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Button } from '@/components/ui/button';
export function TextareaWithLabel() {
return (
<div className="grid w-full gap-1.5">
{/* <Label htmlFor="message">Your message</Label> */}
<Textarea className='border-none min-h-[60px]' placeholder="Type / to see a list of commands" id="message" />
</div>
)
}
interface ChatOptions { interface ChatOptions {
[key: string]: string [key: string]: string
} }
const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple']; const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple'];
interface ChatBodyDataProps {
chatOptionsData: ChatOptions | null;
setTitle: (title: string) => void;
setConversationID?: (conversationId: string) => void;
}
function ChatBodyData({ chatOptionsData }: { chatOptionsData: ChatOptions | null }) {
function ChatBodyData(props: ChatBodyDataProps) {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const conversationId = searchParams.get('conversationId'); const conversationId = searchParams.get('conversationId');
const [showReferencePanel, setShowReferencePanel] = useState(true);
const [referencePanelData, setReferencePanelData] = useState<SingleChatMessage | null>(null); if (conversationId && props.setConversationID) {
props.setConversationID(conversationId);
}
if (!conversationId) { if (!conversationId) {
return ( return (
<div className={styles.suggestions}> <div className={styles.suggestions}>
{chatOptionsData && Object.entries(chatOptionsData).map(([key, value]) => ( {props.chatOptionsData && Object.entries(props.chatOptionsData).map(([key, value]) => (
<SuggestionCard <SuggestionCard
key={key} key={key}
title={`/${key}`} title={`/${key}`}
@@ -42,20 +65,12 @@ function ChatBodyData({ chatOptionsData }: { chatOptionsData: ChatOptions | null
} }
return ( return (
<div className={(hasValidReferences(referencePanelData) && showReferencePanel) ? styles.chatBody : styles.chatBodyFull}> <div className={false ? styles.chatBody : styles.chatBodyFull}>
<ChatHistory conversationId={conversationId} setReferencePanelData={setReferencePanelData} setShowReferencePanel={setShowReferencePanel} /> <ChatHistory conversationId={conversationId} setTitle={props.setTitle} />
{
(hasValidReferences(referencePanelData) && showReferencePanel) &&
<ReferencePanel referencePanelData={referencePanelData} setShowReferencePanel={setShowReferencePanel} />
}
</div> </div>
); );
} }
function Loading() {
return <h2>🌀 Loading...</h2>;
}
function handleChatInput(e: React.FormEvent<HTMLInputElement>) { function handleChatInput(e: React.FormEvent<HTMLInputElement>) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
console.log(target.value); console.log(target.value);
@@ -63,7 +78,10 @@ function handleChatInput(e: React.FormEvent<HTMLInputElement>) {
export default function Chat() { export default function Chat() {
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null); const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
const [isLoading, setLoading] = useState(true) const [isLoading, setLoading] = useState(true);
const [title, setTitle] = useState('Chat');
const [conversationId, setConversationID] = useState<string | null>(null);
const [chatWS, setChatWS] = useState<WebSocket | null>(null);
useEffect(() => { useEffect(() => {
fetch('/api/chat/options') fetch('/api/chat/options')
@@ -80,27 +98,60 @@ export default function Chat() {
console.error(err); console.error(err);
return; return;
}); });
}, []); }, []);
useEffect(() => {
(async () => {
if (conversationId) {
const newWS = await setupWebSocket(conversationId);
setChatWS(newWS);
}
})();
}, [conversationId]);
if (isLoading) {
return <Loading />;
}
return ( return (
<div className={styles.main + " " + styles.chatLayout}> <div className={styles.main + " " + styles.chatLayout}>
<div className={styles.sidePanel}> <div className={styles.sidePanel}>
<SidePanel /> <SidePanel webSocketConnected={chatWS !== null} />
</div> </div>
<div className={styles.chatBox}>
<title> <title>
Khoj AI - Chat Khoj AI - Chat
</title> </title>
<NavMenu selected="Chat" /> <div className={styles.chatBox}>
<NavMenu selected="Chat" title={title} />
<div className={styles.chatBoxBody}>
<div> <div>
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<ChatBodyData chatOptionsData={chatOptionsData} /> <ChatBodyData chatOptionsData={chatOptionsData} setTitle={setTitle} setConversationID={setConversationID} />
</Suspense> </Suspense>
</div> </div>
<div className={styles.inputBox}> {/* <div className={styles.agentIndicator}>
<input className={styles.inputBox} type="text" placeholder="Type here..." onInput={(e) => handleChatInput(e)} /> <a className='no-underline' href="/agents?agent=khoj" target="_blank" rel="noreferrer">
<button className={styles.inputBox}>Send</button> <Lightbulb color='var(--khoj-orange)' weight='fill' />
<span className='text-neutral-600'>Khoj</span>
</a>
</div> */}
<div className={`${styles.inputBox} bg-background align-middle items-center justify-center`}>
<Button className="!bg-transparent !hover:bg-transparent p-0 h-auto text-3xl">
<FileArrowUp fill="hsla(var(--secondary-foreground))"/>
</Button>
<TextareaWithLabel />
<Button className="!bg-transparent !hover:bg-transparent p-0 h-auto text-3xl">
<Microphone fill="hsla(var(--secondary-foreground))"/>
</Button>
<Button className="bg-orange-300 hover:bg-orange-500 rounded-full p-0 h-auto text-3xl">
<ArrowCircleUp/>
</Button>
{/* <input className={styles.inputBox} type="text" placeholder="Type / to see a list of commands" onInput={(e) => handleChatInput(e)} /> */}
{/* <button className={styles.inputBox}>Send</button> */}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,110 @@
import { Context, OnlineContextData } from "../components/chatMessage/chatMessage";
interface ResponseWithReferences {
context?: Context[];
online?: {
[key: string]: OnlineContextData
}
response?: string;
}
function handleCompiledReferences(chunk: string, currentResponse: string) {
const rawReference = chunk.split("### compiled references:")[1];
const rawResponse = chunk.split("### compiled references:")[0];
let references: ResponseWithReferences = {};
// Set the initial response
references.response = currentResponse + rawResponse;
const rawReferenceAsJson = JSON.parse(rawReference);
if (rawReferenceAsJson instanceof Array) {
references.context = rawReferenceAsJson;
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
references.online = rawReferenceAsJson;
}
return references;
}
async function sendChatStream(
message: string,
conversationId: string,
setIsLoading: (loading: boolean) => void,
setInitialResponse: (response: string) => void,
setInitialReferences: (references: ResponseWithReferences) => void) {
setIsLoading(true);
// Send a message to the chat server to verify the fact
const chatURL = "/api/chat";
const apiURL = `${chatURL}?q=${encodeURIComponent(message)}&client=web&stream=true&conversation_id=${conversationId}`;
try {
const response = await fetch(apiURL);
if (!response.body) throw new Error("No response body found");
const reader = response.body?.getReader();
let decoder = new TextDecoder();
let result = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
let chunk = decoder.decode(value, { stream: true });
if (chunk.includes("### compiled references:")) {
const references = handleCompiledReferences(chunk, result);
if (references.response) {
result = references.response;
setInitialResponse(references.response);
setInitialReferences(references);
}
} else {
result += chunk;
setInitialResponse(result);
}
}
} catch (error) {
console.error("Error verifying statement: ", error);
} finally {
setIsLoading(false);
}
}
export function sendChatWS(websocket: WebSocket, message: string) {
websocket.send(message);
}
export const setupWebSocket = async (conversationId: string) => {
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = process.env.NODE_ENV === 'production' ? window.location.host : 'localhost:42110';
let webSocketUrl = `${wsProtocol}//${host}/api/chat/ws`;
if (conversationId === null) return null;
if (conversationId) {
webSocketUrl += `?conversation_id=${conversationId}`;
}
console.log("WebSocket URL: ", webSocketUrl);
const chatWS = new WebSocket(webSocketUrl);
chatWS.onopen = () => {
console.log('WebSocket connection established');
};
chatWS.onmessage = (event) => {
console.log(event.data);
};
chatWS.onerror = (error) => {
console.error('WebSocket error: ', error);
};
chatWS.onclose = () => {
console.log('WebSocket connection closed');
};
return chatWS;
};

View File

@@ -7,6 +7,16 @@ div.chatHistory {
div.chatLayout { div.chatLayout {
height: 80vh; height: 80vh;
overflow-y: auto; overflow-y: auto;
/* width: 80%; */
margin: 0 auto; margin: 0 auto;
} }
div.agentIndicator a {
display: flex;
text-align: center;
align-content: center;
align-items: center;
}
div.agentIndicator {
padding: 10px;
}

View File

@@ -5,10 +5,18 @@ import { useRef, useEffect, useState } from 'react';
import ChatMessage, { ChatHistoryData, SingleChatMessage } from '../chatMessage/chatMessage'; import ChatMessage, { ChatHistoryData, SingleChatMessage } from '../chatMessage/chatMessage';
import ReferencePanel, { hasValidReferences} from '../referencePanel/referencePanel';
import { ScrollArea } from "@/components/ui/scroll-area"
import renderMathInElement from 'katex/contrib/auto-render'; import renderMathInElement from 'katex/contrib/auto-render';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
import 'highlight.js/styles/github.css' import 'highlight.js/styles/github.css'
import Loading from '../loading/loading';
import { Lightbulb } from "@phosphor-icons/react";
interface ChatResponse { interface ChatResponse {
status: string; status: string;
response: ChatHistoryData; response: ChatHistoryData;
@@ -20,8 +28,7 @@ interface ChatHistory {
interface ChatHistoryProps { interface ChatHistoryProps {
conversationId: string; conversationId: string;
setReferencePanelData: Function; setTitle: (title: string) => void;
setShowReferencePanel: Function;
} }
@@ -31,6 +38,8 @@ export default function ChatHistory(props: ChatHistoryProps) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const chatHistoryRef = useRef(null); const chatHistoryRef = useRef(null);
const [showReferencePanel, setShowReferencePanel] = useState(true);
const [referencePanelData, setReferencePanelData] = useState<SingleChatMessage | null>(null);
useEffect(() => { useEffect(() => {
@@ -38,10 +47,12 @@ export default function ChatHistory(props: ChatHistoryProps) {
.then(response => response.json()) .then(response => response.json())
.then((chatData: ChatResponse) => { .then((chatData: ChatResponse) => {
setLoading(false); setLoading(false);
// Render chat options, if any // Render chat options, if any
if (chatData) { if (chatData) {
console.log(chatData); console.log(chatData);
setData(chatData.response); setData(chatData.response);
props.setTitle(chatData.response.slug);
} }
}) })
.catch(err => { .catch(err => {
@@ -78,23 +89,50 @@ export default function ChatHistory(props: ChatHistoryProps) {
}, []); }, []);
if (isLoading) { if (isLoading) {
return <h2>🌀 Loading...</h2>; return <Loading />;
}
function constructAgentLink() {
if (!data || !data.agent || !data.agent.slug) return `/agents`;
return `/agents?agent=${data.agent.slug}`
}
function constructAgentAvatar() {
if (!data || !data.agent || !data.agent.avatar) return `/avatar.png`;
return data.agent.avatar;
}
function constructAgentName() {
if (!data || !data.agent || !data.agent.name) return `Agent`;
return data.agent.name;
} }
return ( return (
<div className={styles.main + " " + styles.chatLayout}> <ScrollArea className={`h-[80vh]`}>
<div ref={ref}> <div ref={ref}>
<div className={styles.chatHistory} ref={chatHistoryRef}> <div className={styles.chatHistory} ref={chatHistoryRef}>
{(data && data.chat) && data.chat.map((chatMessage, index) => ( {(data && data.chat) && data.chat.map((chatMessage, index) => (
<ChatMessage <ChatMessage
key={index} key={index}
chatMessage={chatMessage} chatMessage={chatMessage}
setReferencePanelData={props.setReferencePanelData} setReferencePanelData={setReferencePanelData}
setShowReferencePanel={props.setShowReferencePanel} setShowReferencePanel={setShowReferencePanel}
customClassName='fullHistory'
borderLeftColor='orange-400'
/> />
))} ))}
{
(hasValidReferences(referencePanelData) && showReferencePanel) &&
<ReferencePanel referencePanelData={referencePanelData} setShowReferencePanel={setShowReferencePanel} />
}
<div className={`${styles.agentIndicator}`}>
<a className='no-underline mx-2 flex' href={constructAgentLink()} target="_blank" rel="noreferrer">
<Lightbulb color='orange' weight='fill' />
<span className='text-neutral-600'>{constructAgentName()}</span>
</a>
</div> </div>
</div> </div>
</div> </div>
</ScrollArea>
) )
} }

View File

@@ -1,12 +1,34 @@
div.chatMessageContainer { div.chatMessageContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 12px;
border-radius: 16px;
padding: 16px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03);
}
div.chatMessageWrapper {
padding-left: 24px;
}
div.khojfullHistory {
border-color: var(--border-color);
border-width: 1px;
padding-left: 24px;
}
div.youfullHistory {
max-width: 80%;
}
div.chatMessageContainer.youfullHistory {
padding-left: 0px;
} }
div.you { div.you {
color: var(--frosted-background-color); background-color: var(--frosted-background-color);
background-color: var(--intense-green);
align-self: flex-end; align-self: flex-end;
border-radius: 16px;
} }
div.khoj { div.khoj {
@@ -15,6 +37,11 @@ div.khoj {
align-self: flex-start; align-self: flex-start;
} }
div.khojChatMessage {
padding-top: 8px;
padding-left: 16px;
}
div.chatMessageContainer img { div.chatMessageContainer img {
width: 50%; width: 50%;
} }
@@ -42,21 +69,25 @@ div.chatFooter {
div.chatButtons { div.chatButtons {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
border: var(--border-color) 1px solid;
border-radius: 16px;
position: relative;
bottom: -28px;
background-color: hsla(var(--background));
} }
div.chatFooter button { div.chatFooter button {
cursor: pointer; cursor: pointer;
background-color: var(--calm-blue); color: hsl(var(--muted-foreground));
color: var(--main-text-color);
border: none; border: none;
border-radius: 0.5rem; border-radius: 16px;
padding: 0.25rem; padding: 4px;
margin-left: 0.5rem; margin-left: 4px;
margin-right: 4px;
} }
div.chatFooter button:hover { div.chatFooter button:hover {
background-color: var(--frosted-background-color); background-color: var(--frosted-background-color);
color: var(--intense-green);
} }
div.chatTimestamp { div.chatTimestamp {
@@ -70,7 +101,6 @@ button.codeCopyButton {
} }
button.codeCopyButton:hover { button.codeCopyButton:hover {
background-color: var(--intense-green);
color: var(--frosted-background-color); color: var(--frosted-background-color);
} }
@@ -78,3 +108,10 @@ div.feedbackButtons img,
button.copyButton img { button.copyButton img {
width: 24px; width: 24px;
} }
@media screen and (max-width: 768px) {
div.youfullHistory {
max-width: 100%;
}
}

View File

@@ -12,6 +12,8 @@ import 'highlight.js/styles/github.css'
import { hasValidReferences } from '../referencePanel/referencePanel'; import { hasValidReferences } from '../referencePanel/referencePanel';
import { ThumbsUp, ThumbsDown, Copy } from '@phosphor-icons/react';
const md = new markdownIt({ const md = new markdownIt({
html: true, html: true,
linkify: true, linkify: true,
@@ -103,31 +105,19 @@ export interface ChatHistoryData {
function FeedbackButtons() { function FeedbackButtons() {
return ( return (
<div className={styles.feedbackButtons}> <div className={`${styles.feedbackButtons} flex align-middle justify-center items-center`}>
<button className={styles.thumbsUpButton}> <button className={styles.thumbsUpButton}>
<Image <ThumbsUp color='hsl(var(--muted-foreground))' />
src="/thumbs-up.svg"
alt="Thumbs Up"
width={24}
height={24}
priority
/>
</button> </button>
<button className={styles.thumbsDownButton}> <button className={styles.thumbsDownButton}>
<Image <ThumbsDown color='hsl(var(--muted-foreground))' />
src="/thumbs-down.svg"
alt="Thumbs Down"
width={24}
height={24}
priority
/>
</button> </button>
</div> </div>
) )
} }
function onClickMessage(event: React.MouseEvent<any>, chatMessage: SingleChatMessage, setReferencePanelData: Function, setShowReferencePanel: Function) { function onClickMessage(event: React.MouseEvent<any>, chatMessage: SingleChatMessage, setReferencePanelData: Function, setShowReferencePanel: Function) {
event.preventDefault(); console.log("Clicked on message", chatMessage);
setReferencePanelData(chatMessage); setReferencePanelData(chatMessage);
setShowReferencePanel(true); setShowReferencePanel(true);
} }
@@ -136,6 +126,8 @@ interface ChatMessageProps {
chatMessage: SingleChatMessage; chatMessage: SingleChatMessage;
setReferencePanelData: Function; setReferencePanelData: Function;
setShowReferencePanel: Function; setShowReferencePanel: Function;
customClassName?: string;
borderLeftColor?: string;
} }
export default function ChatMessage(props: ChatMessageProps) { export default function ChatMessage(props: ChatMessageProps) {
@@ -204,13 +196,36 @@ export default function ChatMessage(props: ChatMessageProps) {
let referencesValid = hasValidReferences(props.chatMessage); let referencesValid = hasValidReferences(props.chatMessage);
function constructClasses(chatMessage: SingleChatMessage) {
let classes = [styles.chatMessageContainer];
classes.push(styles[chatMessage.by]);
if (props.customClassName) {
classes.push(styles[`${chatMessage.by}${props.customClassName}`])
}
return classes.join(' ');
}
function chatMessageWrapperClasses(chatMessage: SingleChatMessage) {
let classes = [styles.chatMessageWrapper];
classes.push(styles[chatMessage.by]);
if (chatMessage.by === "khoj") {
classes.push(`border-l-4 border-opacity-50 border-l-orange-500 border-l-${props.borderLeftColor}`);
}
return classes.join(' ');
}
return ( return (
<div <div
className={`${styles.chatMessageContainer} ${styles[props.chatMessage.by]}`} className={constructClasses(props.chatMessage)}
onClick={props.chatMessage.by === "khoj" ? (event) => onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel) : undefined}> onClick={props.chatMessage.by === "khoj" ? (event) => onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel) : undefined}>
{/* <div className={styles.chatFooter}> */} {/* <div className={styles.chatFooter}> */}
{/* {props.chatMessage.by} */} {/* {props.chatMessage.by} */}
{/* </div> */} {/* </div> */}
<div className={chatMessageWrapperClasses(props.chatMessage)}>
<div ref={messageRef} className={styles.chatMessage} dangerouslySetInnerHTML={{ __html: markdownRendered }} /> <div ref={messageRef} className={styles.chatMessage} dangerouslySetInnerHTML={{ __html: markdownRendered }} />
{/* Add a copy button, thumbs up, and thumbs down buttons */} {/* Add a copy button, thumbs up, and thumbs down buttons */}
<div className={styles.chatFooter}> <div className={styles.chatFooter}>
@@ -232,20 +247,8 @@ export default function ChatMessage(props: ChatMessageProps) {
}}> }}>
{ {
copySuccess ? copySuccess ?
<Image <Copy color='green' />
src="/copy-button-success.svg" : <Copy color='hsl(var(--muted-foreground))' />
alt="Checkmark"
width={24}
height={24}
priority
/>
: <Image
src="/copy-button.svg"
alt="Copy"
width={24}
height={24}
priority
/>
} }
</button> </button>
{ {
@@ -254,5 +257,6 @@ export default function ChatMessage(props: ChatMessageProps) {
</div> </div>
</div> </div>
</div> </div>
</div>
) )
} }

View File

@@ -0,0 +1,24 @@
/* HTML: <div class="loader"></div> */
.loader {
--c: conic-gradient(from -90deg, hsla(var(--secondary)) 90deg, #0000 0);
background: var(--c), var(--c);
background-size: 40% 40%;
animation: l19 1s infinite alternate;
}
@keyframes l19 {
0%,
10% {
background-position: 0 0, 0 calc(100%/3)
}
50% {
background-position: 0 0, calc(100%/3) calc(100%/3)
}
90%,
100% {
background-position: 0 0, calc(100%/3) 0
}
}

View File

@@ -0,0 +1,26 @@
import styles from './loading.module.css';
export default function Loading() {
// return (
// <div className={`${styles.loading} h-[100vh] flex items-center justify-center`}>
// <button type="button" className="bg-indigo-500" disabled>
// Loading...
// <span className="relative flex h-3 w-3">
// <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
// <span className="relative inline-flex rounded-full h-3 w-3 bg-sky-500"></span>
// </span>
// </button>
// </div>
// )
return (
<div className={`${styles.loader} h-[100vh] flex items-center justify-center`}></div>
);
return (
<div className="h-[100vh] flex items-center justify-center">
<h2 className="text-4xl text-black animate-bounce">
Loading...
</h2>
</div>
)
}

View File

@@ -11,26 +11,19 @@ menu.menu a {
gap: 4px; gap: 4px;
} }
menu.menu a.selected { a.selected {
background-color: var(--primary-hover); background-color: hsl(var(--accent));
}
menu.menu a:hover {
background-color: var(--primary-hover);
}
menu.menu {
display: flex;
justify-content: space-around;
padding: 0;
margin: 0;
} }
div.titleBar { div.titleBar {
display: grid; display: flex;
grid-template-columns: 1fr auto; padding-left: 12px;
padding: 16px 0; padding-right: 32px;
margin: auto; padding-top: 16px;
padding-bottom: 16px;
justify-content: space-between;
align-content: space-evenly;
align-items: start;
} }
div.titleBar menu { div.titleBar menu {
@@ -75,14 +68,6 @@ div.settingsMenuOptions {
border-radius: 8px; border-radius: 8px;
} }
div.settingsMenuOptions a {
padding: 4px;
}
div.settingsMenuUsername {
font-weight: bold;
}
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
menu.menu span { menu.menu span {
display: none; display: none;
@@ -91,4 +76,8 @@ div.settingsMenuUsername {
div.settingsMenuOptions { div.settingsMenuOptions {
right: 4px; right: 4px;
} }
div.titleBar {
padding: 8px;
}
} }

View File

@@ -1,106 +1,125 @@
'use client' 'use client'
import styles from './navMenu.module.css'; import styles from './navMenu.module.css';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useAuthenticatedData, UserProfile } from '@/app/common/auth'; import { useAuthenticatedData, UserProfile } from '@/app/common/auth';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarTrigger,
} from "@/components/ui/menubar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface NavMenuProps { interface NavMenuProps {
selected: string; selected: string;
showLogo?: boolean;
title?: string;
} }
function SettingsMenu(props: UserProfile) { export default function NavMenu(props: NavMenuProps) {
const [showSettings, setShowSettings] = useState(false);
const userData = useAuthenticatedData();
const [displayTitle, setDisplayTitle] = useState<string>(props.title || props.selected.toUpperCase());
const [isMobileWidth, setIsMobileWidth] = useState(false);
useEffect(() => {
setIsMobileWidth(window.innerWidth < 768);
setDisplayTitle(props.title || props.selected.toUpperCase());
}, [props.title]);
return ( return (
<div className={styles.settingsMenu}> <div className={styles.titleBar}>
<div className={styles.settingsMenuProfile} onClick={() => setShowSettings(!showSettings)}> <div className={`text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8`}>
<Image {displayTitle && <h2 className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden`} >{displayTitle}</h2>}
src={props.photo || "/agents.svg"}
alt={props.username}
width={50}
height={50}
/>
</div> </div>
{showSettings && ( {
<div className={styles.settingsMenuOptions}> isMobileWidth ?
<div className={styles.settingsMenuUsername}>{props.username}</div> <DropdownMenu>
<DropdownMenuTrigger>=</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>Chat</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>Agents</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>Automations</Link>
</DropdownMenuItem>
{userData && <>
<DropdownMenuSeparator />
<DropdownMenuLabel>Profile</DropdownMenuLabel>
<DropdownMenuItem>
<Link href="/config">Settings</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href="https://docs.khoj.dev">Help</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href="/auth/logout">Logout</Link>
</DropdownMenuItem>
</>}
</DropdownMenuContent>
</DropdownMenu>
:
<Menubar className='items-top'>
<MenubarMenu>
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>
<MenubarTrigger>Chat</MenubarTrigger>
</Link>
</MenubarMenu>
<MenubarMenu>
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>
<MenubarTrigger>Agents</MenubarTrigger>
</Link>
</MenubarMenu>
<MenubarMenu>
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>
<MenubarTrigger>Automations</MenubarTrigger>
</Link>
</MenubarMenu>
{userData &&
<MenubarMenu>
<MenubarTrigger>Profile</MenubarTrigger>
<MenubarContent>
<MenubarItem>
<Link href="/config"> <Link href="/config">
Settings Settings
</Link> </Link>
<Link href="https://github.com/khoj-ai/khoj"> </MenubarItem>
Github <MenubarSeparator />
</Link> <MenubarItem>
<Link href="https://docs.khoj.dev"> <Link href="https://docs.khoj.dev">
Help Help
</Link> </Link>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>
<Link href="/auth/logout"> <Link href="/auth/logout">
Logout Logout
</Link> </Link>
</div> </MenubarItem>
)} </MenubarContent>
</div> </MenubarMenu>
); }
</Menubar>
} }
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> </div>
) )
} }

View File

@@ -8,7 +8,6 @@ div.panel {
} }
div.panel a { div.panel a {
color: var(--intense-green);
text-decoration: underline; text-decoration: underline;
} }
@@ -29,3 +28,13 @@ div.singleReference {
background-color: var(--frosted-background-color); background-color: var(--frosted-background-color);
margin-top: 8px; margin-top: 8px;
} }
@media screen and (max-width: 768px) {
div.panel {
padding: 0.5rem;
}
div.singleReference {
padding: 4px;
}
}

View File

@@ -26,7 +26,7 @@ export function hasValidReferences(referencePanelData: SingleChatMessage | null)
(referencePanelData.onlineContext && Object.keys(referencePanelData.onlineContext).length > 0 && (referencePanelData.onlineContext && Object.keys(referencePanelData.onlineContext).length > 0 &&
Object.values(referencePanelData.onlineContext).some( Object.values(referencePanelData.onlineContext).some(
(onlineContextData) => (onlineContextData) =>
(onlineContextData.webpages && onlineContextData.webpages.length > 0)|| onlineContextData.answerBox || onlineContextData.peopleAlsoAsk || onlineContextData.knowledgeGraph)) (onlineContextData.webpages && onlineContextData.webpages.length > 0) || onlineContextData.answerBox || onlineContextData.peopleAlsoAsk || onlineContextData.knowledgeGraph || onlineContextData.organic ))
) )
); );
} }
@@ -98,6 +98,7 @@ function OnlineReferences(props: { onlineContext: OnlineContextData, query: stri
const answerBox = props.onlineContext.answerBox; const answerBox = props.onlineContext.answerBox;
const peopleAlsoAsk = props.onlineContext.peopleAlsoAsk; const peopleAlsoAsk = props.onlineContext.peopleAlsoAsk;
const knowledgeGraph = props.onlineContext.knowledgeGraph; const knowledgeGraph = props.onlineContext.knowledgeGraph;
const organic = props.onlineContext.organic;
return ( return (
<div className={styles.singleReference}> <div className={styles.singleReference}>
@@ -126,6 +127,24 @@ function OnlineReferences(props: { onlineContext: OnlineContextData, query: stri
</div> </div>
) )
} }
{
organic && organic.map((organicData, index) => {
return (
<div className={styles.onlineReference} key={index}>
<div className={styles.onlineReferenceTitle}>
<a href={organicData.link} target="_blank" rel="noreferrer">
{organicData.title}
</a>
</div>
<div className={styles.onlineReferenceContent}>
<div>
{organicData.snippet}
</div>
</div>
</div>
)
})
}
{ {
peopleAlsoAsk && peopleAlsoAsk.map((people, index) => { peopleAlsoAsk && peopleAlsoAsk.map((people, index) => {
return ( return (

View File

@@ -5,69 +5,254 @@ import styles from "./sidePanel.module.css";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { UserProfile } from "@/app/common/auth"; import { UserProfile } from "@/app/common/auth";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import Link from "next/link"; import Link from "next/link";
import useSWR from "swr";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
interface ChatHistory { interface ChatHistory {
conversation_id: string; conversation_id: string;
slug: string; slug: string;
agent_name: string;
agent_avatar: string;
compressed: boolean;
} }
function ChatSession(prop: ChatHistory) { import {
return ( DropdownMenu,
<div key={prop.conversation_id} className={styles.session}> DropdownMenuContent,
<Link href={`/chat?conversationId=${prop.conversation_id}`}> DropdownMenuItem,
<p className={styles.session}>{prop.slug || "New Conversation 🌱"}</p> DropdownMenuTrigger,
</Link> } from "@/components/ui/dropdown-menu";
</div>
); import { Pencil, Trash, Share } from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";
interface GroupedChatHistory {
[key: string]: ChatHistory[];
} }
interface ChatSessionsModalProps { function renameConversation(conversationId: string, newTitle: string) {
data: ChatHistory[]; const editUrl = `/api/chat/title?client=web&conversation_id=${conversationId}&title=${newTitle}`;
setIsExpanded: React.Dispatch<React.SetStateAction<boolean>>;
}
function ChatSessionsModal({data, setIsExpanded}: ChatSessionsModalProps) { fetch(editUrl, {
return ( method: 'POST',
<div className={styles.modalSessionsList}> headers: {
<div className={styles.content}> 'Content-Type': 'application/json',
{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(response => response.json())
.then((data: ChatHistory[]) => { .then(data => {
setLoading(false); console.log(data);
// Render chat options, if any
if (data) {
setData(data);
setDataToShow(data.slice(0, 5));
}
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
return; return;
}); });
}
function shareConversation(conversationId: string) {
const shareUrl = `/api/chat/share?client=web&conversation_id=${conversationId}`;
fetch(shareUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(err => {
console.error(err);
return;
});
}
function deleteConversation(conversationId: string) {
const deleteUrl = `/api/chat/delete?client=web&conversation_id=${conversationId}`;
fetch(deleteUrl, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(err => {
console.error(err);
return;
});
}
interface ChatSessionActionMenuProps {
conversationId: string;
}
function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger>:</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => renameConversation(props.conversationId, 'New Title')}>
<Pencil className="mr-2 h-4 w-4" />Rename
</Button>
</DropdownMenuItem>
<DropdownMenuItem>
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => shareConversation(props.conversationId)}>
<Share className="mr-2 h-4 w-4" />Share
</Button>
</DropdownMenuItem>
<DropdownMenuItem>
<Button className="p-0 text-sm h-auto text-rose-300 hover:text-rose-400" variant={'ghost'} onClick={() => deleteConversation(props.conversationId)}>
<Trash className="mr-2 h-4 w-4" />Delete
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
function ChatSession(props: ChatHistory) {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
key={props.conversation_id}
className={`${styles.session} ${props.compressed ? styles.compressed : '!max-w-full'} ${isHovered ? `${styles.sessionHover}` : ''}`}>
<Link href={`/chat?conversationId=${props.conversation_id}`}>
<p className={styles.session}>{props.slug || "New Conversation 🌱"}</p>
</Link>
<ChatSessionActionMenu conversationId={props.conversation_id} />
</div>
);
}
interface ChatSessionsModalProps {
data: GroupedChatHistory | null;
}
function ChatSessionsModal({ data }: ChatSessionsModalProps) {
return (
<Dialog>
<DialogTrigger
className="flex text-left text-medium text-gray-500 hover:text-gray-900 cursor-pointer">
Show All
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>All Conversations</DialogTitle>
<DialogDescription>
<ScrollArea className="h-[500px] w-[450px] rounded-md border p-4">
{data && Object.keys(data).map((agentName) => (
<div key={agentName}>
<h3 className={`grid grid-flow-col auto-cols-max gap-2`}>
<img src={data[agentName][0].agent_avatar} alt={agentName} width={24} height={24} />
{agentName}
</h3>
{data[agentName].map((chatHistory) => (
<ChatSession
compressed={false}
key={chatHistory.conversation_id}
conversation_id={chatHistory.conversation_id}
slug={chatHistory.slug}
agent_avatar={chatHistory.agent_avatar}
agent_name={chatHistory.agent_name} />
))}
</div>
))}
</ScrollArea>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
);
}
const fetchChatHistory = async (url: string) => {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return response.json();
};
export const useChatHistoryRecentFetchRequest = (url: string) => {
const { data, error } = useSWR<ChatHistory[]>(url, fetchChatHistory);
return {
data,
isLoading: !error && !data,
isError: error,
};
};
interface SidePanelProps {
webSocketConnected?: boolean;
}
export default function SidePanel(props: SidePanelProps) {
const [data, setData] = useState<ChatHistory[] | null>(null);
const [organizedData, setOrganizedData] = useState<GroupedChatHistory | null>(null);
const [subsetOrganizedData, setSubsetOrganizedData] = useState<GroupedChatHistory | null>(null);
const [isLoading, setLoading] = useState(true)
const [enabled, setEnabled] = useState(false);
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
const { data: chatHistory } = useChatHistoryRecentFetchRequest('/api/chat/sessions');
useEffect(() => {
if (chatHistory) {
setData(chatHistory);
const groupedData: GroupedChatHistory = {};
const subsetOrganizedData: GroupedChatHistory = {};
let numAdded = 0;
chatHistory.forEach((chatHistory) => {
if (!groupedData[chatHistory.agent_name]) {
groupedData[chatHistory.agent_name] = [];
}
groupedData[chatHistory.agent_name].push(chatHistory);
// Add to subsetOrganizedData if less than 8
if (numAdded < 8) {
if (!subsetOrganizedData[chatHistory.agent_name]) {
subsetOrganizedData[chatHistory.agent_name] = [];
}
subsetOrganizedData[chatHistory.agent_name].push(chatHistory);
numAdded++;
}
});
setSubsetOrganizedData(subsetOrganizedData);
setOrganizedData(groupedData);
}
}, [chatHistory]);
useEffect(() => {
fetch('/api/v1/user', { method: 'GET' }) fetch('/api/v1/user', { method: 'GET' })
.then(response => response.json()) .then(response => response.json())
@@ -84,58 +269,71 @@ export default function SidePanel() {
<div className={`${styles.panel}`}> <div className={`${styles.panel}`}>
{ {
enabled ? enabled ?
<div> <div className={`${styles.panelWrapper}`}>
<div className={`${styles.expanded}`}> <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)}> <button className={styles.button} onClick={() => setEnabled(false)}>
{/* Push Close Icon */} {/* 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> <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> </button>
<h3>Recent Conversations</h3>
</div> </div>
<ScrollArea className="h-[40vh] w-[14rem]">
<div className={styles.sessionsList}> <div className={styles.sessionsList}>
{dataToShow && dataToShow.map((chatHistory) => ( {subsetOrganizedData && Object.keys(subsetOrganizedData).map((agentName) => (
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} /> <div key={agentName} className={`my-4`}>
<h3 className={`grid grid-flow-col auto-cols-max gap-2 my-4 font-bold text-sm`}>
<img src={subsetOrganizedData[agentName][0].agent_avatar} alt={agentName} width={24} height={24} />
{agentName}
</h3>
{subsetOrganizedData[agentName].map((chatHistory) => (
<ChatSession
compressed={true}
key={chatHistory.conversation_id}
conversation_id={chatHistory.conversation_id}
slug={chatHistory.slug}
agent_avatar={chatHistory.agent_avatar}
agent_name={chatHistory.agent_name} />
))} ))}
</div> </div>
))}
</div>
</ScrollArea>
{ {
(data && data.length > 5) && ( (data && data.length > 5) && (
(isExpanded) ? <ChatSessionsModal data={organizedData} />
<ChatSessionsModal data={data} setIsExpanded={setIsExpanded} />
:
<button className={styles.showMoreButton} onClick={() => {
setIsExpanded(true);
}}>
Show All
</button>
) )
} }
{userProfile &&
<div className={styles.profile}>
<Avatar>
<AvatarImage src={userProfile.photo} alt="user profile" />
<AvatarFallback>
{userProfile.username[0]}
</AvatarFallback>
</Avatar>
<div className={styles.profileDetails}>
<p>{userProfile?.username}</p>
{/* Connected Indicator */}
<div className="flex gap-2 items-center">
<div className={`inline-flex h-4 w-4 rounded-full opacity-75 ${props.webSocketConnected ? 'bg-green-500' : 'bg-rose-500'}`}></div>
<p className="text-muted-foreground text-sm">
{props.webSocketConnected ? "Connected" : "Disconnected"}
</p>
</div>
</div>
</div>
}
</div> </div>
: :
<div> <div>
<div className={`${styles.collapsed}`}> <div className={`${styles.collapsed}`}>
{userProfile && {userProfile &&
<div className={`${styles.profile}`}> <div className={styles.profile}>
<img <Avatar className="max-w-6 max-h-6 rounded-full">
className={styles.profile} <AvatarImage src={userProfile.photo} alt="user profile" />
src={userProfile.photo} <AvatarFallback>
alt="profile" {userProfile.username[0]}
width={24} </AvatarFallback>
height={24} </Avatar>
/>
</div> </div>
} }
<button className={styles.button} onClick={() => setEnabled(true)}> <button className={styles.button} onClick={() => setEnabled(true)}>

View File

@@ -2,9 +2,27 @@ div.session {
padding: 0.5rem; padding: 0.5rem;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--main-text-color);
cursor: pointer; cursor: pointer;
max-width: 14rem; max-width: 14rem;
font-size: medium;
display: grid;
grid-template-columns: minmax(auto, 350px) 1fr;
}
div.compressed {
grid-template-columns: minmax(auto, 12rem) 1fr 1fr;
}
div.sessionHover {
background-color: var(--secondary-accent);
}
div.session:hover {
background-color: var(--secondary-accent);
}
div.session a {
text-decoration: none;
} }
button.button { button.button {
@@ -17,9 +35,6 @@ button.button {
} }
button.showMoreButton { button.showMoreButton {
background: var(--intense-green);
border: none;
color: var(--frosted-background-color);
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 8px; padding: 8px;
} }
@@ -28,9 +43,8 @@ div.panel {
display: grid; display: grid;
grid-auto-flow: row; grid-auto-flow: row;
padding: 1rem; padding: 1rem;
border-radius: 1rem; background-color: hsla(var(--primary-foreground));
background-color: var(--calm-blue); background-color: var(--secondary-background-color);
color: var(--main-text-color);
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
max-width: auto; max-width: auto;
@@ -47,14 +61,12 @@ div.collapsed {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
div.session:hover {
background-color: var(--calmer-blue);
}
p.session { p.session {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-align: left;
font-size: small;
} }
div.header { div.header {
@@ -62,10 +74,18 @@ div.header {
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
} }
img.profile { div.profile {
width: 24px; display: grid;
height: 24px; grid-template-columns: auto 1fr;
border-radius: 50%; gap: 1rem;
align-items: center;
align-self: flex-end;
margin-top: auto;
}
div.panelWrapper {
display: flex;
flex-direction: column;
} }
@@ -96,3 +116,13 @@ div.modalSessionsList div.session {
max-width: 100%; max-width: 100%;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@media screen and (max-width: 768px) {
div.panel {
padding: 0.5rem;
}
div.singleReference {
padding: 4px;
}
}

View File

@@ -26,7 +26,14 @@
--ring: 209.1 100% 40.8%; --ring: 209.1 100% 40.8%;
--radius: 0.5rem; --radius: 0.5rem;
--font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important; --font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important;
/* Khoj Custom Colors */
--primary-hover: #fee285; --primary-hover: #fee285;
--frosted-background-color: #F5F3F2;
--secondary-background-color: #F7F7F5;
--secondary-accent: #EDEDED;
--khoj-orange: #FFE7D1;
--border-color: #e2e2e2;
} }
.dark { .dark {

View File

@@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@@ -0,0 +1,236 @@
"use client"
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const MenubarMenu = MenubarPrimitive.Menu
const MenubarGroup = MenubarPrimitive.Group
const MenubarPortal = MenubarPrimitive.Portal
const MenubarSub = MenubarPrimitive.Sub
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
className
)}
{...props}
/>
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...props}
/>
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
)
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}

View File

@@ -0,0 +1,128 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}

View File

@@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

View File

@@ -17,9 +17,15 @@
"windowsexport": "yarn build && xcopy out ..\\..\\khoj\\interface\\built /E /Y && yarn windowscollectstatic" "windowsexport": "yarn build && xcopy out ..\\..\\khoj\\interface\\built /E /Y && yarn windowscollectstatic"
}, },
"dependencies": { "dependencies": {
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@types/katex": "^0.16.7", "@types/katex": "^0.16.7",
"@types/markdown-it": "^14.1.1", "@types/markdown-it": "^14.1.1",

View File

@@ -318,6 +318,33 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f"
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
"@floating-ui/core@^1.0.0":
version "1.6.3"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.3.tgz#5e7bb92843f47fd1d8dcb9b3cc3c243aaed54f95"
integrity sha512-1ZpCvYf788/ZXOhRQGFxnYQOVgeU+pi0i+d0Ow34La7qjIXETi6RNswGVKkA6KcDO8/+Ysu2E/CeUmmeEBDvTg==
dependencies:
"@floating-ui/utils" "^0.2.3"
"@floating-ui/dom@^1.0.0":
version "1.6.6"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.6.tgz#be54c1ab2d19112ad323e63dbeb08185fed0ffd3"
integrity sha512-qiTYajAnh3P+38kECeffMSQgbvXty2VB6rS+42iWR4FPIlZjLK84E9qtLnMTLIpPz2znD/TaFqaiavMUrS+Hcw==
dependencies:
"@floating-ui/core" "^1.0.0"
"@floating-ui/utils" "^0.2.3"
"@floating-ui/react-dom@^2.0.0":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.1.tgz#cca58b6b04fc92b4c39288252e285e0422291fb0"
integrity sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==
dependencies:
"@floating-ui/dom" "^1.0.0"
"@floating-ui/utils@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.3.tgz#506fcc73f730affd093044cb2956c31ba6431545"
integrity sha512-XGndio0l5/Gvd6CLIABvsav9HHezgDFFhDfHk1bvLfr9ni8dojqLSvBbotJEjmIwNHL7vK4QzBJTdBRoB+c1ww==
"@humanwhocodes/config-array@^0.11.14": "@humanwhocodes/config-array@^0.11.14":
version "0.11.14" version "0.11.14"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
@@ -459,11 +486,21 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@phosphor-icons/react@^2.1.7":
version "2.1.7"
resolved "https://registry.yarnpkg.com/@phosphor-icons/react/-/react-2.1.7.tgz#b11a4b25849b7e3849970b688d9fe91e5d4fd8d7"
integrity sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==
"@pkgjs/parseargs@^0.11.0": "@pkgjs/parseargs@^0.11.0":
version "0.11.0" version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@radix-ui/number@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.0.tgz#1e95610461a09cdf8bb05c152e76ca1278d5da46"
integrity sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==
"@radix-ui/primitive@1.1.0": "@radix-ui/primitive@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2" resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
@@ -481,6 +518,33 @@
"@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-slot" "1.1.0" "@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-arrow@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz#744f388182d360b86285217e43b6c63633f39e7a"
integrity sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==
dependencies:
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-avatar@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.0.tgz#457c81334c93f4608df15f081e7baa286558d6a2"
integrity sha512-Q/PbuSMk/vyAd/UoIShVGZ7StHHeRFYU7wXmi5GV+8cLXflZAEpHL/F697H1klrzxKXNtZ97vWiC0q3RKUH8UA==
dependencies:
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-collection@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed"
integrity sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==
dependencies:
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-compose-refs@1.1.0": "@radix-ui/react-compose-refs@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74" resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74"
@@ -511,6 +575,11 @@
aria-hidden "^1.1.1" aria-hidden "^1.1.1"
react-remove-scroll "2.5.7" react-remove-scroll "2.5.7"
"@radix-ui/react-direction@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc"
integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==
"@radix-ui/react-dismissable-layer@1.1.0": "@radix-ui/react-dismissable-layer@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz#2cd0a49a732372513733754e6032d3fb7988834e" resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz#2cd0a49a732372513733754e6032d3fb7988834e"
@@ -522,6 +591,19 @@
"@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0" "@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-dropdown-menu@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.1.tgz#3dc578488688250dbbe109d9ff2ca28a9bca27ec"
integrity sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-menu" "2.1.1"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-focus-guards@1.1.0": "@radix-ui/react-focus-guards@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13" resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13"
@@ -550,6 +632,82 @@
dependencies: dependencies:
"@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-menu@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.1.tgz#bd623ace0e1ae1ac78023a505fec0541d59fb346"
integrity sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-collection" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-dismissable-layer" "1.1.0"
"@radix-ui/react-focus-guards" "1.1.0"
"@radix-ui/react-focus-scope" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-popper" "1.2.0"
"@radix-ui/react-portal" "1.1.1"
"@radix-ui/react-presence" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-roving-focus" "1.1.0"
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.7"
"@radix-ui/react-menubar@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-menubar/-/react-menubar-1.1.1.tgz#e126514cb1c46e0a4f9fba7d016e578cc4e41f22"
integrity sha512-V05Hryq/BE2m+rs8d5eLfrS0jmSWSDHEbG7jEyLA5D5J9jTvWj/o3v3xDN9YsOlH6QIkJgiaNDaP+S4T1rdykw==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-collection" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-menu" "2.1.1"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-roving-focus" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-navigation-menu@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.0.tgz#884c9b9fd141cc5db257bd3f6bf3b84e349c6617"
integrity sha512-OQ8tcwAOR0DhPlSY3e4VMXeHiol7la4PPdJWhhwJiJA+NLX0SaCaonOkRnI3gCDHoZ7Fo7bb/G6q25fRM2Y+3Q==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-collection" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-dismissable-layer" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-presence" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-visually-hidden" "1.1.0"
"@radix-ui/react-popper@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz#a3e500193d144fe2d8f5d5e60e393d64111f2a7a"
integrity sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==
dependencies:
"@floating-ui/react-dom" "^2.0.0"
"@radix-ui/react-arrow" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-use-rect" "1.1.0"
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/rect" "1.1.0"
"@radix-ui/react-portal@1.1.1": "@radix-ui/react-portal@1.1.1":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.1.tgz#1957f1eb2e1aedfb4a5475bd6867d67b50b1d15f" resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.1.tgz#1957f1eb2e1aedfb4a5475bd6867d67b50b1d15f"
@@ -573,6 +731,36 @@
dependencies: dependencies:
"@radix-ui/react-slot" "1.1.0" "@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-roving-focus@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e"
integrity sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-collection" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-scroll-area@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz#50b24b0fc9ada151d176395bcf47b2ec68feada5"
integrity sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==
dependencies:
"@radix-ui/number" "1.1.0"
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-presence" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.1.0": "@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84" resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84"
@@ -604,6 +792,37 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w== integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==
"@radix-ui/react-use-previous@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz#d4dd37b05520f1d996a384eb469320c2ada8377c"
integrity sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==
"@radix-ui/react-use-rect@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88"
integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==
dependencies:
"@radix-ui/rect" "1.1.0"
"@radix-ui/react-use-size@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b"
integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==
dependencies:
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-visually-hidden@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz#ad47a8572580f7034b3807c8e6740cd41038a5a2"
integrity sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==
dependencies:
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/rect@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438"
integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==
"@rushstack/eslint-patch@^1.3.3": "@rushstack/eslint-patch@^1.3.3":
version "1.10.3" version "1.10.3"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz#391d528054f758f81e53210f1a1eebcf1a8b1d20" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz#391d528054f758f81e53210f1a1eebcf1a8b1d20"

View File

@@ -644,7 +644,11 @@ class ConversationAdapters:
@staticmethod @staticmethod
def get_conversation_sessions(user: KhojUser, client_application: ClientApplication = None): def get_conversation_sessions(user: KhojUser, client_application: ClientApplication = None):
return Conversation.objects.filter(user=user, client=client_application).order_by("-updated_at") return (
Conversation.objects.filter(user=user, client=client_application)
.prefetch_related("agent")
.order_by("-updated_at")
)
@staticmethod @staticmethod
async def aset_conversation_title( async def aset_conversation_title(

View File

@@ -2,7 +2,7 @@ import json
import logging import logging
import math import math
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Dict, Optional
from urllib.parse import unquote from urllib.parse import unquote
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
@@ -15,7 +15,6 @@ from websockets import ConnectionClosedOK
from khoj.database.adapters import ( from khoj.database.adapters import (
ConversationAdapters, ConversationAdapters,
DataStoreAdapters,
EntryAdapters, EntryAdapters,
FileObjectAdapters, FileObjectAdapters,
PublicConversationAdapters, PublicConversationAdapters,
@@ -95,53 +94,6 @@ def get_file_filter(request: Request, conversation_id: str) -> Response:
return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200) return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
class FactCheckerStoreDataFormat(BaseModel):
factToVerify: str
response: str
references: Any
childReferences: List[Any]
runId: str
modelUsed: Dict[str, Any]
class FactCheckerStoreData(BaseModel):
runId: str
storeData: FactCheckerStoreDataFormat
@api_chat.post("/store/factchecker", response_class=Response)
@requires(["authenticated"])
async def store_factchecker(request: Request, common: CommonQueryParams, data: FactCheckerStoreData):
user = request.user.object
update_telemetry_state(
request=request,
telemetry_type="api",
api="store_factchecker",
**common.__dict__,
)
fact_checker_key = f"factchecker_{data.runId}"
await DataStoreAdapters.astore_data(data.storeData.model_dump_json(), fact_checker_key, user, private=False)
return Response(content=json.dumps({"status": "ok"}), media_type="application/json", status_code=200)
@api_chat.get("/store/factchecker", response_class=Response)
async def get_factchecker(request: Request, common: CommonQueryParams, runId: str):
update_telemetry_state(
request=request,
telemetry_type="api",
api="read_factchecker",
**common.__dict__,
)
fact_checker_key = f"factchecker_{runId}"
data = await DataStoreAdapters.aretrieve_public_data(fact_checker_key)
if data is None:
return Response(status_code=404)
return Response(content=json.dumps(data.value), media_type="application/json", status_code=200)
@api_chat.post("/conversation/file-filters", response_class=Response) @api_chat.post("/conversation/file-filters", response_class=Response)
@requires(["authenticated"]) @requires(["authenticated"])
def add_file_filter(request: Request, filter: FilterRequest): def add_file_filter(request: Request, filter: FilterRequest):
@@ -418,15 +370,26 @@ def duplicate_chat_history_public_conversation(
def chat_sessions( def chat_sessions(
request: Request, request: Request,
common: CommonQueryParams, common: CommonQueryParams,
recent: Optional[bool] = False,
): ):
user = request.user.object user = request.user.object
# Load Conversation Sessions # Load Conversation Sessions
sessions = ConversationAdapters.get_conversation_sessions(user, request.user.client_app).values_list( conversations = ConversationAdapters.get_conversation_sessions(user, request.user.client_app)
"id", "slug", "title" if recent:
) conversations = conversations[:8]
session_values = [{"conversation_id": session[0], "slug": session[2] or session[1]} for session in sessions] sessions = conversations.values_list("id", "slug", "title", "agent__slug", "agent__name", "agent__avatar")
session_values = [
{
"conversation_id": session[0],
"slug": session[2] or session[1],
"agent_name": session[4],
"agent_avatar": session[5],
}
for session in sessions
]
update_telemetry_state( update_telemetry_state(
request=request, request=request,