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);
}}>
<Image
src="Close.svg"
src="close.svg"
alt="Close"
width={24}
height={24} />

View file

@ -12,23 +12,27 @@ div.main {
div.inputBox {
display: grid;
grid-template-columns: 1fr auto;
padding: 1rem;
border-radius: 1rem;
background-color: #f5f5f5;
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.1);
grid-template-columns: auto 1fr auto auto;
border: 1px solid var(--border-color);
border-radius: 16px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03);
margin-bottom: 20px;
gap: 12px;
padding-left: 20px;
padding-right: 20px;
}
input.inputBox {
border: none;
}
input.inputBox:focus {
outline: none;
background-color: transparent;
}
input.inputBox:focus {
border: none;
outline: none;
background-color: transparent;
div.inputBox:focus {
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
}
div.chatBodyFull {
@ -65,9 +69,7 @@ div.chatLayout {
div.chatBox {
display: grid;
gap: 1rem;
height: 100%;
padding: 1rem;
}
div.titleBar {
@ -75,6 +77,25 @@ div.titleBar {
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) {
div.chatBody {
grid-template-columns: 0fr 1fr;
@ -84,3 +105,22 @@ div.titleBar {
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 SidePanel from '../components/sidePanel/chatHistorySidePanel';
import ChatHistory from '../components/chatHistory/chatHistory';
import { SingleChatMessage } from '../components/chatMessage/chatMessage';
import NavMenu from '../components/navMenu/navMenu';
import { useSearchParams } from 'next/navigation'
import ReferencePanel, { hasValidReferences } from '../components/referencePanel/referencePanel';
import Loading from '../components/loading/loading';
import { setupWebSocket } from '../common/chatFunctions';
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 {
[key: string]: string
}
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 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) {
return (
<div className={styles.suggestions}>
{chatOptionsData && Object.entries(chatOptionsData).map(([key, value]) => (
{props.chatOptionsData && Object.entries(props.chatOptionsData).map(([key, value]) => (
<SuggestionCard
key={key}
title={`/${key}`}
@ -42,20 +65,12 @@ function ChatBodyData({ chatOptionsData }: { chatOptionsData: ChatOptions | null
}
return (
<div className={(hasValidReferences(referencePanelData) && showReferencePanel) ? styles.chatBody : styles.chatBodyFull}>
<ChatHistory conversationId={conversationId} setReferencePanelData={setReferencePanelData} setShowReferencePanel={setShowReferencePanel} />
{
(hasValidReferences(referencePanelData) && showReferencePanel) &&
<ReferencePanel referencePanelData={referencePanelData} setShowReferencePanel={setShowReferencePanel} />
}
<div className={false ? styles.chatBody : styles.chatBodyFull}>
<ChatHistory conversationId={conversationId} setTitle={props.setTitle} />
</div>
);
}
function Loading() {
return <h2>🌀 Loading...</h2>;
}
function handleChatInput(e: React.FormEvent<HTMLInputElement>) {
const target = e.target as HTMLInputElement;
console.log(target.value);
@ -63,7 +78,10 @@ function handleChatInput(e: React.FormEvent<HTMLInputElement>) {
export default function Chat() {
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(() => {
fetch('/api/chat/options')
@ -80,27 +98,60 @@ export default function Chat() {
console.error(err);
return;
});
}, []);
useEffect(() => {
(async () => {
if (conversationId) {
const newWS = await setupWebSocket(conversationId);
setChatWS(newWS);
}
})();
}, [conversationId]);
if (isLoading) {
return <Loading />;
}
return (
<div className={styles.main + " " + styles.chatLayout}>
<div className={styles.sidePanel}>
<SidePanel />
<SidePanel webSocketConnected={chatWS !== null} />
</div>
<div className={styles.chatBox}>
<title>
Khoj AI - Chat
</title>
<NavMenu selected="Chat" />
<div className={styles.chatBox}>
<NavMenu selected="Chat" title={title} />
<div className={styles.chatBoxBody}>
<div>
<Suspense fallback={<Loading />}>
<ChatBodyData chatOptionsData={chatOptionsData} />
<ChatBodyData chatOptionsData={chatOptionsData} setTitle={setTitle} setConversationID={setConversationID} />
</Suspense>
</div>
<div className={styles.inputBox}>
<input className={styles.inputBox} type="text" placeholder="Type here..." onInput={(e) => handleChatInput(e)} />
<button className={styles.inputBox}>Send</button>
{/* <div className={styles.agentIndicator}>
<a className='no-underline' href="/agents?agent=khoj" target="_blank" rel="noreferrer">
<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>

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 {
height: 80vh;
overflow-y: auto;
/* width: 80%; */
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 ReferencePanel, { hasValidReferences} from '../referencePanel/referencePanel';
import { ScrollArea } from "@/components/ui/scroll-area"
import renderMathInElement from 'katex/contrib/auto-render';
import 'katex/dist/katex.min.css';
import 'highlight.js/styles/github.css'
import Loading from '../loading/loading';
import { Lightbulb } from "@phosphor-icons/react";
interface ChatResponse {
status: string;
response: ChatHistoryData;
@ -20,8 +28,7 @@ interface ChatHistory {
interface ChatHistoryProps {
conversationId: string;
setReferencePanelData: Function;
setShowReferencePanel: Function;
setTitle: (title: string) => void;
}
@ -31,6 +38,8 @@ export default function ChatHistory(props: ChatHistoryProps) {
const ref = useRef<HTMLDivElement>(null);
const chatHistoryRef = useRef(null);
const [showReferencePanel, setShowReferencePanel] = useState(true);
const [referencePanelData, setReferencePanelData] = useState<SingleChatMessage | null>(null);
useEffect(() => {
@ -38,10 +47,12 @@ export default function ChatHistory(props: ChatHistoryProps) {
.then(response => response.json())
.then((chatData: ChatResponse) => {
setLoading(false);
// Render chat options, if any
if (chatData) {
console.log(chatData);
setData(chatData.response);
props.setTitle(chatData.response.slug);
}
})
.catch(err => {
@ -78,23 +89,50 @@ export default function ChatHistory(props: ChatHistoryProps) {
}, []);
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 (
<div className={styles.main + " " + styles.chatLayout}>
<ScrollArea className={`h-[80vh]`}>
<div ref={ref}>
<div className={styles.chatHistory} ref={chatHistoryRef}>
{(data && data.chat) && data.chat.map((chatMessage, index) => (
<ChatMessage
key={index}
chatMessage={chatMessage}
setReferencePanelData={props.setReferencePanelData}
setShowReferencePanel={props.setShowReferencePanel}
setReferencePanelData={setReferencePanelData}
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>
</ScrollArea>
)
}

View file

@ -1,12 +1,34 @@
div.chatMessageContainer {
display: flex;
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 {
color: var(--frosted-background-color);
background-color: var(--intense-green);
background-color: var(--frosted-background-color);
align-self: flex-end;
border-radius: 16px;
}
div.khoj {
@ -15,6 +37,11 @@ div.khoj {
align-self: flex-start;
}
div.khojChatMessage {
padding-top: 8px;
padding-left: 16px;
}
div.chatMessageContainer img {
width: 50%;
}
@ -42,21 +69,25 @@ div.chatFooter {
div.chatButtons {
display: flex;
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 {
cursor: pointer;
background-color: var(--calm-blue);
color: var(--main-text-color);
color: hsl(var(--muted-foreground));
border: none;
border-radius: 0.5rem;
padding: 0.25rem;
margin-left: 0.5rem;
border-radius: 16px;
padding: 4px;
margin-left: 4px;
margin-right: 4px;
}
div.chatFooter button:hover {
background-color: var(--frosted-background-color);
color: var(--intense-green);
}
div.chatTimestamp {
@ -70,7 +101,6 @@ button.codeCopyButton {
}
button.codeCopyButton:hover {
background-color: var(--intense-green);
color: var(--frosted-background-color);
}
@ -78,3 +108,10 @@ div.feedbackButtons img,
button.copyButton img {
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 { ThumbsUp, ThumbsDown, Copy } from '@phosphor-icons/react';
const md = new markdownIt({
html: true,
linkify: true,
@ -103,31 +105,19 @@ export interface ChatHistoryData {
function FeedbackButtons() {
return (
<div className={styles.feedbackButtons}>
<div className={`${styles.feedbackButtons} flex align-middle justify-center items-center`}>
<button className={styles.thumbsUpButton}>
<Image
src="/thumbs-up.svg"
alt="Thumbs Up"
width={24}
height={24}
priority
/>
<ThumbsUp color='hsl(var(--muted-foreground))' />
</button>
<button className={styles.thumbsDownButton}>
<Image
src="/thumbs-down.svg"
alt="Thumbs Down"
width={24}
height={24}
priority
/>
<ThumbsDown color='hsl(var(--muted-foreground))' />
</button>
</div>
)
}
function onClickMessage(event: React.MouseEvent<any>, chatMessage: SingleChatMessage, setReferencePanelData: Function, setShowReferencePanel: Function) {
event.preventDefault();
console.log("Clicked on message", chatMessage);
setReferencePanelData(chatMessage);
setShowReferencePanel(true);
}
@ -136,6 +126,8 @@ interface ChatMessageProps {
chatMessage: SingleChatMessage;
setReferencePanelData: Function;
setShowReferencePanel: Function;
customClassName?: string;
borderLeftColor?: string;
}
export default function ChatMessage(props: ChatMessageProps) {
@ -204,13 +196,36 @@ export default function ChatMessage(props: ChatMessageProps) {
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 (
<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}>
{/* <div className={styles.chatFooter}> */}
{/* {props.chatMessage.by} */}
{/* </div> */}
<div className={chatMessageWrapperClasses(props.chatMessage)}>
<div ref={messageRef} className={styles.chatMessage} dangerouslySetInnerHTML={{ __html: markdownRendered }} />
{/* Add a copy button, thumbs up, and thumbs down buttons */}
<div className={styles.chatFooter}>
@ -232,20 +247,8 @@ export default function ChatMessage(props: ChatMessageProps) {
}}>
{
copySuccess ?
<Image
src="/copy-button-success.svg"
alt="Checkmark"
width={24}
height={24}
priority
/>
: <Image
src="/copy-button.svg"
alt="Copy"
width={24}
height={24}
priority
/>
<Copy color='green' />
: <Copy color='hsl(var(--muted-foreground))' />
}
</button>
{
@ -254,5 +257,6 @@ export default function ChatMessage(props: ChatMessageProps) {
</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;
}
menu.menu a.selected {
background-color: var(--primary-hover);
}
menu.menu a:hover {
background-color: var(--primary-hover);
}
menu.menu {
display: flex;
justify-content: space-around;
padding: 0;
margin: 0;
a.selected {
background-color: hsl(var(--accent));
}
div.titleBar {
display: grid;
grid-template-columns: 1fr auto;
padding: 16px 0;
margin: auto;
display: flex;
padding-left: 12px;
padding-right: 32px;
padding-top: 16px;
padding-bottom: 16px;
justify-content: space-between;
align-content: space-evenly;
align-items: start;
}
div.titleBar menu {
@ -75,14 +68,6 @@ div.settingsMenuOptions {
border-radius: 8px;
}
div.settingsMenuOptions a {
padding: 4px;
}
div.settingsMenuUsername {
font-weight: bold;
}
@media screen and (max-width: 600px) {
menu.menu span {
display: none;
@ -91,4 +76,8 @@ div.settingsMenuUsername {
div.settingsMenuOptions {
right: 4px;
}
div.titleBar {
padding: 8px;
}
}

View file

@ -1,106 +1,125 @@
'use client'
import styles from './navMenu.module.css';
import Image from 'next/image';
import Link from 'next/link';
import { useAuthenticatedData, UserProfile } from '@/app/common/auth';
import { useState } from 'react';
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 {
selected: string;
showLogo?: boolean;
title?: string;
}
function SettingsMenu(props: UserProfile) {
const [showSettings, setShowSettings] = useState(false);
export default function NavMenu(props: NavMenuProps) {
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 (
<div className={styles.settingsMenu}>
<div className={styles.settingsMenuProfile} onClick={() => setShowSettings(!showSettings)}>
<Image
src={props.photo || "/agents.svg"}
alt={props.username}
width={50}
height={50}
/>
<div className={styles.titleBar}>
<div className={`text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8`}>
{displayTitle && <h2 className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden`} >{displayTitle}</h2>}
</div>
{showSettings && (
<div className={styles.settingsMenuOptions}>
<div className={styles.settingsMenuUsername}>{props.username}</div>
{
isMobileWidth ?
<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">
Settings
</Link>
<Link href="https://github.com/khoj-ai/khoj">
Github
</Link>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>
<Link href="https://docs.khoj.dev">
Help
</Link>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>
<Link href="/auth/logout">
Logout
</Link>
</div>
)}
</div>
);
</MenubarItem>
</MenubarContent>
</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>
)
}

View file

@ -8,7 +8,6 @@ div.panel {
}
div.panel a {
color: var(--intense-green);
text-decoration: underline;
}
@ -29,3 +28,13 @@ div.singleReference {
background-color: var(--frosted-background-color);
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 &&
Object.values(referencePanelData.onlineContext).some(
(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 peopleAlsoAsk = props.onlineContext.peopleAlsoAsk;
const knowledgeGraph = props.onlineContext.knowledgeGraph;
const organic = props.onlineContext.organic;
return (
<div className={styles.singleReference}>
@ -126,6 +127,24 @@ function OnlineReferences(props: { onlineContext: OnlineContextData, query: stri
</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) => {
return (

View file

@ -5,69 +5,254 @@ import styles from "./sidePanel.module.css";
import { useEffect, useState } from "react";
import { UserProfile } from "@/app/common/auth";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
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 {
conversation_id: string;
slug: string;
agent_name: string;
agent_avatar: string;
compressed: boolean;
}
function ChatSession(prop: ChatHistory) {
return (
<div key={prop.conversation_id} className={styles.session}>
<Link href={`/chat?conversationId=${prop.conversation_id}`}>
<p className={styles.session}>{prop.slug || "New Conversation 🌱"}</p>
</Link>
</div>
);
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Pencil, Trash, Share } from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";
interface GroupedChatHistory {
[key: string]: ChatHistory[];
}
interface ChatSessionsModalProps {
data: ChatHistory[];
setIsExpanded: React.Dispatch<React.SetStateAction<boolean>>;
}
function renameConversation(conversationId: string, newTitle: string) {
const editUrl = `/api/chat/title?client=web&conversation_id=${conversationId}&title=${newTitle}`;
function ChatSessionsModal({data, setIsExpanded}: ChatSessionsModalProps) {
return (
<div className={styles.modalSessionsList}>
<div className={styles.content}>
{data.map((chatHistory) => (
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} />
))}
<button className={styles.showMoreButton} onClick={() => setIsExpanded(false)}>
Close
</button>
</div>
</div>
);
}
export default function SidePanel() {
const [data, setData] = useState<ChatHistory[] | null>(null);
const [dataToShow, setDataToShow] = useState<ChatHistory[] | null>(null);
const [isLoading, setLoading] = useState(true)
const [enabled, setEnabled] = useState(false);
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
useEffect(() => {
fetch('/api/chat/sessions', { method: 'GET' })
fetch(editUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then((data: ChatHistory[]) => {
setLoading(false);
// Render chat options, if any
if (data) {
setData(data);
setDataToShow(data.slice(0, 5));
}
.then(data => {
console.log(data);
})
.catch(err => {
console.error(err);
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' })
.then(response => response.json())
@ -84,58 +269,71 @@ export default function SidePanel() {
<div className={`${styles.panel}`}>
{
enabled ?
<div>
<div className={`${styles.panelWrapper}`}>
<div className={`${styles.expanded}`}>
<div className={`${styles.profile}`}>
{ userProfile &&
<div className={styles.profile}>
<img
className={styles.profile}
src={userProfile.photo}
alt="profile"
width={24}
height={24}
/>
<p>{userProfile?.username}</p>
</div>
}
</div>
<button className={styles.button} onClick={() => setEnabled(false)}>
{/* Push Close Icon */}
<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M8.70710678,12 L19.5,12 C19.7761424,12 20,12.2238576 20,12.5 C20,12.7761424 19.7761424,13 19.5,13 L8.70710678,13 L11.8535534,16.1464466 C12.0488155,16.3417088 12.0488155,16.6582912 11.8535534,16.8535534 C11.6582912,17.0488155 11.3417088,17.0488155 11.1464466,16.8535534 L7.14644661,12.8535534 C6.95118446,12.6582912 6.95118446,12.3417088 7.14644661,12.1464466 L11.1464466,8.14644661 C11.3417088,7.95118446 11.6582912,7.95118446 11.8535534,8.14644661 C12.0488155,8.34170876 12.0488155,8.65829124 11.8535534,8.85355339 L8.70710678,12 L8.70710678,12 Z M4,5.5 C4,5.22385763 4.22385763,5 4.5,5 C4.77614237,5 5,5.22385763 5,5.5 L5,19.5 C5,19.7761424 4.77614237,20 4.5,20 C4.22385763,20 4,19.7761424 4,19.5 L4,5.5 Z"></path> </g></svg>
</button>
<h3>Recent Conversations</h3>
</div>
<ScrollArea className="h-[40vh] w-[14rem]">
<div className={styles.sessionsList}>
{dataToShow && dataToShow.map((chatHistory) => (
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} />
{subsetOrganizedData && Object.keys(subsetOrganizedData).map((agentName) => (
<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>
</ScrollArea>
{
(data && data.length > 5) && (
(isExpanded) ?
<ChatSessionsModal data={data} setIsExpanded={setIsExpanded} />
:
<button className={styles.showMoreButton} onClick={() => {
setIsExpanded(true);
}}>
Show All
</button>
<ChatSessionsModal data={organizedData} />
)
}
{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 className={`${styles.collapsed}`}>
{userProfile &&
<div className={`${styles.profile}`}>
<img
className={styles.profile}
src={userProfile.photo}
alt="profile"
width={24}
height={24}
/>
<div className={styles.profile}>
<Avatar className="max-w-6 max-h-6 rounded-full">
<AvatarImage src={userProfile.photo} alt="user profile" />
<AvatarFallback>
{userProfile.username[0]}
</AvatarFallback>
</Avatar>
</div>
}
<button className={styles.button} onClick={() => setEnabled(true)}>

View file

@ -2,9 +2,27 @@ div.session {
padding: 0.5rem;
margin-bottom: 0.25rem;
border-radius: 0.5rem;
color: var(--main-text-color);
cursor: pointer;
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 {
@ -17,9 +35,6 @@ button.button {
}
button.showMoreButton {
background: var(--intense-green);
border: none;
color: var(--frosted-background-color);
border-radius: 0.5rem;
padding: 8px;
}
@ -28,9 +43,8 @@ div.panel {
display: grid;
grid-auto-flow: row;
padding: 1rem;
border-radius: 1rem;
background-color: var(--calm-blue);
color: var(--main-text-color);
background-color: hsla(var(--primary-foreground));
background-color: var(--secondary-background-color);
height: 100%;
overflow-y: auto;
max-width: auto;
@ -47,14 +61,12 @@ div.collapsed {
grid-template-columns: 1fr;
}
div.session:hover {
background-color: var(--calmer-blue);
}
p.session {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
text-align: left;
font-size: small;
}
div.header {
@ -62,10 +74,18 @@ div.header {
grid-template-columns: 1fr auto;
}
img.profile {
width: 24px;
height: 24px;
border-radius: 50%;
div.profile {
display: grid;
grid-template-columns: auto 1fr;
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%;
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%;
--radius: 0.5rem;
--font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important;
/* Khoj Custom Colors */
--primary-hover: #fee285;
--frosted-background-color: #F5F3F2;
--secondary-background-color: #F7F7F5;
--secondary-accent: #EDEDED;
--khoj-orange: #FFE7D1;
--border-color: #e2e2e2;
}
.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"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.7",
"@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-dropdown-menu": "^2.1.1",
"@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",
"@types/katex": "^0.16.7",
"@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"
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":
version "0.11.14"
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"
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":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
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":
version "1.1.0"
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-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":
version "1.1.0"
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"
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":
version "1.1.0"
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-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":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13"
@ -550,6 +632,82 @@
dependencies:
"@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":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.1.tgz#1957f1eb2e1aedfb4a5475bd6867d67b50b1d15f"
@ -573,6 +731,36 @@
dependencies:
"@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":
version "1.1.0"
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"
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":
version "1.10.3"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz#391d528054f758f81e53210f1a1eebcf1a8b1d20"

View file

@ -644,7 +644,11 @@ class ConversationAdapters:
@staticmethod
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
async def aset_conversation_title(

View file

@ -2,7 +2,7 @@ import json
import logging
import math
from datetime import datetime
from typing import Any, Dict, List, Optional
from typing import Dict, Optional
from urllib.parse import unquote
from asgiref.sync import sync_to_async
@ -15,7 +15,6 @@ from websockets import ConnectionClosedOK
from khoj.database.adapters import (
ConversationAdapters,
DataStoreAdapters,
EntryAdapters,
FileObjectAdapters,
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)
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)
@requires(["authenticated"])
def add_file_filter(request: Request, filter: FilterRequest):
@ -418,15 +370,26 @@ def duplicate_chat_history_public_conversation(
def chat_sessions(
request: Request,
common: CommonQueryParams,
recent: Optional[bool] = False,
):
user = request.user.object
# Load Conversation Sessions
sessions = ConversationAdapters.get_conversation_sessions(user, request.user.client_app).values_list(
"id", "slug", "title"
)
conversations = ConversationAdapters.get_conversation_sessions(user, request.user.client_app)
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(
request=request,