Make most major changes for an updated chat UI (#843)

- Updated references panel
- Use subtle coloring for chat cards
- Chat streaming with train of thought
- Side panel with limited sessions, expandable
- Manage conversation file filters easily from the side panel
- Updated nav menu, easily go to agents/automations/profile
- Upload data from the chat UI (on click attachment icon)
- Slash command pop-up menu, scrollable and selectable
- Dark mode-enabled
- Mostly mobile friendly
This commit is contained in:
sabaimran
2024-07-14 10:48:06 -07:00
committed by GitHub
parent 02658ad4fd
commit 06dce4729b
46 changed files with 5652 additions and 954 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

@@ -1,6 +1,6 @@
div.main { div.main {
height: 100vh; height: 100vh;
color: black; color: hsla(var(--foreground));
} }
.suggestions { .suggestions {
@@ -11,24 +11,27 @@ div.main {
} }
div.inputBox { div.inputBox {
display: grid; border: 1px solid var(--border-color);
grid-template-columns: 1fr auto; border-radius: 16px;
padding: 1rem; box-shadow: 0 4px 10px var(--box-shadow-color);
border-radius: 1rem; margin-bottom: 20px;
background-color: #f5f5f5; gap: 12px;
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.1); padding-left: 20px;
padding-right: 20px;
align-content: center;
} }
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 {
@@ -54,7 +57,7 @@ div.chatBody {
} }
.inputBox { .inputBox {
color: black; color: hsla(var(--foreground));
} }
div.chatLayout { div.chatLayout {
@@ -65,9 +68,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 +76,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 +104,23 @@ 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;
grid-template-columns: 1fr;
}
}

View File

@@ -16,6 +16,15 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<meta httpEquiv="Content-Security-Policy"
content="default-src 'self' https://assets.khoj.dev;
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
child-src 'none';
object-src 'none';"></meta>
<body className={inter.className}> <body className={inter.className}>
{children} {children}
</body> </body>

View File

@@ -6,29 +6,68 @@ 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 { handleCompiledReferences, handleImageResponse, setupWebSocket } from '../common/chatFunctions';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
interface ChatOptions { import { StreamMessage } from '../components/chatMessage/chatMessage';
[key: string]: string import { welcomeConsole } from '../common/utils';
} import ChatInputArea, { ChatOptions } from '../components/chatInputArea/chatInputArea';
import { useAuthenticatedData } from '../common/auth';
const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple']; const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple'];
interface ChatBodyDataProps {
chatOptionsData: ChatOptions | null;
setTitle: (title: string) => void;
onConversationIdChange?: (conversationId: string) => void;
setQueryToProcess: (query: string) => void;
streamedMessages: StreamMessage[];
setUploadedFiles: (files: string[]) => void;
isMobileWidth?: boolean;
isLoggedIn: boolean;
}
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 [message, setMessage] = useState('');
const [referencePanelData, setReferencePanelData] = useState<SingleChatMessage | null>(null); const [processingMessage, setProcessingMessage] = useState(false);
useEffect(() => {
if (conversationId) {
props.onConversationIdChange?.(conversationId);
}
}, [conversationId, props.onConversationIdChange]);
useEffect(() => {
if (message) {
setProcessingMessage(true);
props.setQueryToProcess(message);
}
}, [message]);
useEffect(() => {
if (props.streamedMessages &&
props.streamedMessages.length > 0 &&
props.streamedMessages[props.streamedMessages.length - 1].completed) {
setProcessingMessage(false);
} else {
setMessage('');
}
}, [props.streamedMessages]);
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}`}
@@ -41,29 +80,111 @@ function ChatBodyData({ chatOptionsData }: { chatOptionsData: ChatOptions | null
); );
} }
return( return (
<div className={(hasValidReferences(referencePanelData) && showReferencePanel) ? styles.chatBody : styles.chatBodyFull}> <>
<ChatHistory conversationId={conversationId} setReferencePanelData={setReferencePanelData} setShowReferencePanel={setShowReferencePanel} /> <div className={false ? styles.chatBody : styles.chatBodyFull}>
{ <ChatHistory
(hasValidReferences(referencePanelData) && showReferencePanel) && conversationId={conversationId}
<ReferencePanel referencePanelData={referencePanelData} setShowReferencePanel={setShowReferencePanel} /> setTitle={props.setTitle}
} pendingMessage={processingMessage ? message : ''}
incomingMessages={props.streamedMessages} />
</div> </div>
<div className={`${styles.inputBox} bg-background align-middle items-center justify-center px-3`}>
<ChatInputArea
isLoggedIn={props.isLoggedIn}
sendMessage={(message) => setMessage(message)}
sendDisabled={processingMessage}
chatOptionsData={props.chatOptionsData}
conversationId={conversationId}
isMobileWidth={props.isMobileWidth}
setUploadedFiles={props.setUploadedFiles} />
</div>
</>
); );
} }
function Loading() {
return <h2>🌀 Loading...</h2>;
}
function handleChatInput(e: React.FormEvent<HTMLInputElement>) {
const target = e.target as HTMLInputElement;
console.log(target.value);
}
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('Khoj AI - Chat');
const [conversationId, setConversationID] = useState<string | null>(null);
const [chatWS, setChatWS] = useState<WebSocket | null>(null);
const [messages, setMessages] = useState<StreamMessage[]>([]);
const [queryToProcess, setQueryToProcess] = useState<string>('');
const [processQuerySignal, setProcessQuerySignal] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
const [isMobileWidth, setIsMobileWidth] = useState(false);
const authenticatedData = useAuthenticatedData();
welcomeConsole();
const handleWebSocketMessage = (event: MessageEvent) => {
let chunk = event.data;
let currentMessage = messages.find(message => !message.completed);
if (!currentMessage) {
console.error("No current message found");
return;
}
// Process WebSocket streamed data
if (chunk === "start_llm_response") {
console.log("Started streaming", new Date());
} else if (chunk === "end_llm_response") {
currentMessage.completed = true;
} else {
// Get the current message
// Process and update state with the new message
if (chunk.includes("application/json")) {
chunk = JSON.parse(chunk);
}
const contentType = chunk["content-type"];
if (contentType === "application/json") {
try {
if (chunk.image || chunk.detail) {
let responseWithReference = handleImageResponse(chunk);
console.log("Image response", responseWithReference);
if (responseWithReference.response) currentMessage.rawResponse = responseWithReference.response;
if (responseWithReference.online) currentMessage.onlineContext = responseWithReference.online;
if (responseWithReference.context) currentMessage.context = responseWithReference.context;
} else if (chunk.type == "status") {
currentMessage.trainOfThought.push(chunk.message);
} else if (chunk.type == "rate_limit") {
console.log("Rate limit message", chunk);
currentMessage.rawResponse = chunk.message;
} else {
console.log("any message", chunk);
}
} catch (error) {
console.error("Error processing message", error);
currentMessage.completed = true;
} finally {
// no-op
}
} else {
// Update the current message with the new chunk
if (chunk && chunk.includes("### compiled references:")) {
let responseWithReference = handleCompiledReferences(chunk, "");
currentMessage.rawResponse += responseWithReference.response;
if (responseWithReference.response) currentMessage.rawResponse = responseWithReference.response;
if (responseWithReference.online) currentMessage.onlineContext = responseWithReference.online;
if (responseWithReference.context) currentMessage.context = responseWithReference.context;
} else {
// If the chunk is not a JSON object, just display it as is
currentMessage.rawResponse += chunk;
}
}
};
// Update the state with the new message, currentMessage
setMessages([...messages]);
}
useEffect(() => { useEffect(() => {
fetch('/api/chat/options') fetch('/api/chat/options')
@@ -72,7 +193,6 @@ export default function Chat() {
setLoading(false); setLoading(false);
// Render chat options, if any // Render chat options, if any
if (data) { if (data) {
console.log(data);
setChatOptionsData(data); setChatOptionsData(data);
} }
}) })
@@ -80,28 +200,92 @@ export default function Chat() {
console.error(err); console.error(err);
return; return;
}); });
setIsMobileWidth(window.innerWidth < 786);
window.addEventListener('resize', () => {
setIsMobileWidth(window.innerWidth < 786);
});
}, []); }, []);
useEffect(() => {
if (chatWS && queryToProcess) {
// Add a new object to the state
const newStreamMessage: StreamMessage = {
rawResponse: "",
trainOfThought: [],
context: [],
onlineContext: {},
completed: false,
timestamp: (new Date()).toISOString(),
rawQuery: queryToProcess || "",
}
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
setProcessQuerySignal(true);
} else {
if (!chatWS) {
console.error("No WebSocket connection available");
}
if (!queryToProcess) {
console.error("No query to process");
}
}
}, [queryToProcess]);
useEffect(() => {
if (processQuerySignal && chatWS) {
setProcessQuerySignal(false);
chatWS.onmessage = handleWebSocketMessage;
chatWS?.send(queryToProcess);
}
}, [processQuerySignal]);
useEffect(() => {
(async () => {
if (conversationId) {
const newWS = await setupWebSocket(conversationId);
setChatWS(newWS);
}
})();
}, [conversationId]);
const handleConversationIdChange = (newConversationId: string) => {
setConversationID(newConversationId);
};
if (isLoading) {
return <Loading />;
}
return ( return (
<div className={styles.main + " " + styles.chatLayout}> <div className={styles.main + " " + styles.chatLayout}>
<title>
{title}
</title>
<div className={styles.sidePanel}> <div className={styles.sidePanel}>
<SidePanel /> <SidePanel
webSocketConnected={chatWS !== null}
conversationId={conversationId}
uploadedFiles={uploadedFiles} />
</div> </div>
<div className={styles.chatBox}> <div className={styles.chatBox}>
<title> <NavMenu selected="Chat" title={title} />
Khoj AI - Chat <div className={styles.chatBoxBody}>
</title>
<NavMenu selected="Chat" />
<div>
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<ChatBodyData chatOptionsData={chatOptionsData} /> <ChatBodyData
isLoggedIn={authenticatedData !== null}
streamedMessages={messages}
chatOptionsData={chatOptionsData}
setTitle={setTitle}
setQueryToProcess={setQueryToProcess}
setUploadedFiles={setUploadedFiles}
isMobileWidth={isMobileWidth}
onConversationIdChange={handleConversationIdChange} />
</Suspense> </Suspense>
</div> </div>
<div className={styles.inputBox}>
<input className={styles.inputBox} type="text" placeholder="Type here..." onInput={(e) => handleChatInput(e)} />
<button className={styles.inputBox}>Send</button>
</div>
</div> </div>
</div> </div>
) )

View File

@@ -0,0 +1,299 @@
import { Context, OnlineContextData } from "../components/chatMessage/chatMessage";
interface ResponseWithReferences {
context?: Context[];
online?: {
[key: string]: OnlineContextData
}
response?: string;
}
export 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 const setupWebSocket = async (conversationId: string, initialMessage?: 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}`;
}
const chatWS = new WebSocket(webSocketUrl);
chatWS.onopen = () => {
console.log('WebSocket connection established');
if (initialMessage) {
chatWS.send(initialMessage);
}
};
chatWS.onmessage = (event) => {
console.log(event.data);
};
chatWS.onerror = (error) => {
console.error('WebSocket error: ', error);
};
chatWS.onclose = () => {
console.log('WebSocket connection closed');
};
return chatWS;
};
export function handleImageResponse(imageJson: any) {
let rawResponse = "";
if (imageJson.image) {
const inferredQuery = imageJson.inferredQueries?.[0] ?? "generated image";
// If response has image field, response is a generated image.
if (imageJson.intentType === "text-to-image") {
rawResponse += `![generated_image](data:image/png;base64,${imageJson.image})`;
} else if (imageJson.intentType === "text-to-image2") {
rawResponse += `![generated_image](${imageJson.image})`;
} else if (imageJson.intentType === "text-to-image-v3") {
rawResponse = `![](data:image/webp;base64,${imageJson.image})`;
}
if (inferredQuery) {
rawResponse += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
}
}
let reference: ResponseWithReferences = {};
if (imageJson.context && imageJson.context.length > 0) {
const rawReferenceAsJson = imageJson.context;
if (rawReferenceAsJson instanceof Array) {
reference.context = rawReferenceAsJson;
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
reference.online = rawReferenceAsJson;
}
}
if (imageJson.detail) {
// The detail field contains the improved image prompt
rawResponse += imageJson.detail;
}
reference.response = rawResponse;
return reference;
}
export function modifyFileFilterForConversation(
conversationId: string | null,
filenames: string[],
setAddedFiles: (files: string[]) => void,
mode: 'add' | 'remove') {
if (!conversationId) {
console.error("No conversation ID provided");
return;
}
const method = mode === 'add' ? 'POST' : 'DELETE';
const body = {
conversation_id: conversationId,
filenames: filenames,
}
const addUrl = `/api/chat/conversation/file-filters/bulk`;
fetch(addUrl, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
.then(response => response.json())
.then(data => {
console.log("ADDEDFILES DATA: ", data);
setAddedFiles(data);
})
.catch(err => {
console.error(err);
return;
});
}
export function uploadDataForIndexing(
files: FileList,
setWarning: (warning: string) => void,
setUploading: (uploading: boolean) => void,
setError: (error: string) => void,
setUploadedFiles?: (files: string[]) => void,
conversationId?: string | null) {
const allowedExtensions = ['text/org', 'text/markdown', 'text/plain', 'text/html', 'application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
const allowedFileEndings = ['org', 'md', 'txt', 'html', 'pdf', 'docx'];
const badFiles: string[] = [];
const goodFiles: File[] = [];
const uploadedFiles: string[] = [];
for (let file of files) {
const fileEnding = file.name.split('.').pop();
if (!file || !file.name || !fileEnding) {
if (file) {
badFiles.push(file.name);
}
} else if ((!allowedExtensions.includes(file.type) && !allowedFileEndings.includes(fileEnding.toLowerCase()))) {
badFiles.push(file.name);
} else {
goodFiles.push(file);
}
}
if (goodFiles.length === 0) {
setWarning("No supported files found");
return;
}
if (badFiles.length > 0) {
setWarning("The following files are not supported yet:\n" + badFiles.join('\n'));
}
const formData = new FormData();
// Create an array of Promises for file reading
const fileReadPromises = Array.from(goodFiles).map(file => {
return new Promise<void>((resolve, reject) => {
let reader = new FileReader();
reader.onload = function (event) {
if (event.target === null) {
reject();
return;
}
let fileContents = event.target.result;
let fileType = file.type;
let fileName = file.name;
if (fileType === "") {
let fileExtension = fileName.split('.').pop();
if (fileExtension === "org") {
fileType = "text/org";
} else if (fileExtension === "md") {
fileType = "text/markdown";
} else if (fileExtension === "txt") {
fileType = "text/plain";
} else if (fileExtension === "html") {
fileType = "text/html";
} else if (fileExtension === "pdf") {
fileType = "application/pdf";
} else {
// Skip this file if its type is not supported
resolve();
return;
}
}
if (fileContents === null) {
reject();
return;
}
let fileObj = new Blob([fileContents], { type: fileType });
formData.append("files", fileObj, file.name);
resolve();
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
});
setUploading(true);
// Wait for all files to be read before making the fetch request
Promise.all(fileReadPromises)
.then(() => {
return fetch("/api/v1/index/update?force=false&client=web", {
method: "POST",
body: formData,
});
})
.then((data) => {
for (let file of goodFiles) {
uploadedFiles.push(file.name);
if (conversationId && setUploadedFiles) {
modifyFileFilterForConversation(conversationId, [file.name], setUploadedFiles, 'add');
}
}
if (setUploadedFiles) setUploadedFiles(uploadedFiles);
})
.catch((error) => {
console.log(error);
setError(`Error uploading file: ${error}`);
})
.finally(() => {
setUploading(false);
});
}

View File

@@ -0,0 +1,17 @@
export function welcomeConsole() {
console.log(`%c %s`, "font-family:monospace", `
__ __ __ __ ______ __ _____ __
/\\ \\/ / /\\ \\_\\ \\ /\\ __ \\ /\\ \\ /\\ __ \\ /\\ \\
\\ \\ _"-. \\ \\ __ \\ \\ \\ \\/\\ \\ _\\_\\ \\ \\ \\ __ \\ \\ \\ \\
\\ \\_\\ \\_\\ \\ \\_\\ \\_\\ \\ \\_____\\ /\\_____\\ \\ \\_\\ \\_\\ \\ \\_\\
\\/_/\\/_/ \\/_/\\/_/ \\/_____/ \\/_____/ \\/_/\\/_/ \\/_/
Greetings traveller,
I am ✨Khoj✨, your open-source, personal AI copilot.
See my source code at https://github.com/khoj-ai/khoj
Read my operating manual at https://docs.khoj.dev
`);
}

View File

@@ -7,6 +7,24 @@ 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;
}
div.trainOfThought {
border: 1px var(--border-color) solid;
border-radius: 16px;
padding: 16px;
margin: 12px;
box-shadow: 0 4px 10px var(--box-shadow-color);
}

View File

@@ -3,11 +3,16 @@
import styles from './chatHistory.module.css'; import styles from './chatHistory.module.css';
import { useRef, useEffect, useState } from 'react'; import { useRef, useEffect, useState } from 'react';
import ChatMessage, { ChatHistoryData, SingleChatMessage } from '../chatMessage/chatMessage'; import ChatMessage, { ChatHistoryData, StreamMessage, TrainOfThought } from '../chatMessage/chatMessage';
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 Loading, { InlineLoading } from '../loading/loading';
import { Lightbulb } from "@phosphor-icons/react";
interface ChatResponse { interface ChatResponse {
status: string; status: string;
@@ -20,42 +25,117 @@ interface ChatHistory {
interface ChatHistoryProps { interface ChatHistoryProps {
conversationId: string; conversationId: string;
setReferencePanelData: Function; setTitle: (title: string) => void;
setShowReferencePanel: Function; incomingMessages?: StreamMessage[];
pendingMessage?: string;
publicConversationSlug?: string;
}
function constructTrainOfThought(trainOfThought: string[], lastMessage: boolean, key: string, completed: boolean = false) {
const lastIndex = trainOfThought.length - 1;
return (
<div className={`${styles.trainOfThought}`} key={key}>
{
!completed &&
<InlineLoading className='float-right' />
}
{trainOfThought.map((train, index) => (
<TrainOfThought key={`train-${index}`} message={train} primary={index === lastIndex && lastMessage && !completed} />
))}
</div>
)
} }
export default function ChatHistory(props: ChatHistoryProps) { export default function ChatHistory(props: ChatHistoryProps) {
const [data, setData] = useState<ChatHistoryData | null>(null); const [data, setData] = useState<ChatHistoryData | null>(null);
const [isLoading, setLoading] = useState(true) const [currentPage, setCurrentPage] = useState(0);
const [hasMoreMessages, setHasMoreMessages] = useState(true);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const chatHistoryRef = useRef(null); const chatHistoryRef = useRef<HTMLDivElement | null>(null);
const sentinelRef = useRef<HTMLDivElement | null>(null);
const [incompleteIncomingMessageIndex, setIncompleteIncomingMessageIndex] = useState<number | null>(null);
const [fetchingData, setFetchingData] = useState(false);
const [isMobileWidth, setIsMobileWidth] = useState(false);
useEffect(() => { useEffect(() => {
window.addEventListener('resize', () => {
fetch(`/api/chat/history?client=web&conversation_id=${props.conversationId}&n=10`) setIsMobileWidth(window.innerWidth < 768);
.then(response => response.json())
.then((chatData: ChatResponse) => {
setLoading(false);
// Render chat options, if any
if (chatData) {
console.log(chatData);
setData(chatData.response);
}
})
.catch(err => {
console.error(err);
return;
}); });
setIsMobileWidth(window.innerWidth < 768);
}, []);
useEffect(() => {
// This function ensures that scrolling to bottom happens after the data (chat messages) has been updated and rendered the first time.
const scrollToBottomAfterDataLoad = () => {
// Assume the data is loading in this scenario.
if (!data?.chat.length) {
setTimeout(() => {
scrollToBottom();
}, 500);
}
};
if (currentPage < 2) {
// Call the function defined above.
scrollToBottomAfterDataLoad();
}
}, [chatHistoryRef.current, data]);
useEffect(() => {
if (!hasMoreMessages || fetchingData) return;
// TODO: A future optimization would be to add a time to delay to re-enabling the intersection observer.
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMoreMessages) {
setFetchingData(true);
fetchMoreMessages(currentPage);
setCurrentPage((prev) => prev + 1);
}
}, { threshold: 1.0 });
if (sentinelRef.current) {
observer.observe(sentinelRef.current);
}
return () => observer.disconnect();
}, [sentinelRef.current, hasMoreMessages, currentPage, fetchingData]);
useEffect(() => {
setHasMoreMessages(true);
setFetchingData(false);
setCurrentPage(0);
setData(null);
}, [props.conversationId]); }, [props.conversationId]);
useEffect(() => {
console.log(props.incomingMessages);
if (props.incomingMessages) {
const lastMessage = props.incomingMessages[props.incomingMessages.length - 1];
if (lastMessage && !lastMessage.completed) {
setIncompleteIncomingMessageIndex(props.incomingMessages.length - 1);
}
}
if (isUserAtBottom()) {
scrollToBottom();
}
}, [props.incomingMessages]);
useEffect(() => { useEffect(() => {
const observer = new MutationObserver((mutationsList, observer) => { const observer = new MutationObserver((mutationsList, observer) => {
// If the addedNodes property has one or more nodes // If the addedNodes property has one or more nodes
for(let mutation of mutationsList) { for (let mutation of mutationsList) {
if(mutation.type === 'childList' && mutation.addedNodes.length > 0) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Call your function here // Call your function here
renderMathInElement(document.body, { renderMathInElement(document.body, {
delimiters: [ delimiters: [
@@ -77,24 +157,175 @@ export default function ChatHistory(props: ChatHistoryProps) {
return () => observer.disconnect(); return () => observer.disconnect();
}, []); }, []);
if (isLoading) { const fetchMoreMessages = (currentPage: number) => {
return <h2>🌀 Loading...</h2>; if (!hasMoreMessages || fetchingData) return;
const nextPage = currentPage + 1;
let conversationFetchURL = '';
if (props.conversationId) {
conversationFetchURL = `/api/chat/history?client=web&conversation_id=${props.conversationId}&n=${10 * nextPage}`;
} else if (props.publicConversationSlug) {
conversationFetchURL = `/api/chat/share/history?client=web&public_conversation_slug=${props.publicConversationSlug}&n=${10 * nextPage}`;
} else {
return;
}
fetch(conversationFetchURL)
.then(response => response.json())
.then((chatData: ChatResponse) => {
props.setTitle(chatData.response.slug);
if (chatData && chatData.response && chatData.response.chat.length > 0) {
if (chatData.response.chat.length === data?.chat.length) {
setHasMoreMessages(false);
setFetchingData(false);
return;
}
setData(chatData.response);
if (currentPage < 2) {
scrollToBottom();
}
setFetchingData(false);
} else {
setHasMoreMessages(false);
}
})
.catch(err => {
console.error(err);
});
};
const scrollToBottom = () => {
if (chatHistoryRef.current) {
chatHistoryRef.current.scrollIntoView(false);
}
}
const isUserAtBottom = () => {
if (!chatHistoryRef.current) return false;
// NOTE: This isn't working. It always seems to return true. This is because
const { scrollTop, scrollHeight, clientHeight } = chatHistoryRef.current as HTMLDivElement;
const threshold = 25; // pixels from the bottom
// Considered at the bottom if within threshold pixels from the bottom
return scrollTop + clientHeight >= scrollHeight - threshold;
}
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;
}
if (!props.conversationId && !props.publicConversationSlug) {
return null;
} }
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}>
<div ref={sentinelRef} style={{ height: '1px' }}>
{fetchingData && <InlineLoading />}
</div>
{(data && data.chat) && data.chat.map((chatMessage, index) => ( {(data && data.chat) && data.chat.map((chatMessage, index) => (
<ChatMessage <ChatMessage
key={index} key={`${index}fullHistory`}
isMobileWidth={isMobileWidth}
chatMessage={chatMessage} chatMessage={chatMessage}
setReferencePanelData={props.setReferencePanelData} customClassName='fullHistory'
setShowReferencePanel={props.setShowReferencePanel} borderLeftColor='orange-500'
/> />
))} ))}
{
props.incomingMessages && props.incomingMessages.map((message, index) => {
return (
<>
<ChatMessage
key={`${index}outgoing`}
isMobileWidth={isMobileWidth}
chatMessage={
{
message: message.rawQuery,
context: [],
onlineContext: {},
created: message.timestamp,
by: "you",
automationId: '',
}
}
customClassName='fullHistory'
borderLeftColor='orange-500' />
{
message.trainOfThought &&
constructTrainOfThought(
message.trainOfThought,
index === incompleteIncomingMessageIndex,
`${index}trainOfThought`, message.completed)
}
<ChatMessage
key={`${index}incoming`}
isMobileWidth={isMobileWidth}
chatMessage={
{
message: message.rawResponse,
context: message.context,
onlineContext: message.onlineContext,
created: message.timestamp,
by: "khoj",
automationId: '',
rawQuery: message.rawQuery,
}
}
customClassName='fullHistory'
borderLeftColor='orange-500'
/>
</>
)
})
}
{
props.pendingMessage &&
<ChatMessage
key={`pendingMessage-${props.pendingMessage.length}`}
isMobileWidth={isMobileWidth}
chatMessage={
{
message: props.pendingMessage,
context: [],
onlineContext: {},
created: (new Date().getTime()).toString(),
by: "you",
automationId: '',
}
}
customClassName='fullHistory'
borderLeftColor='orange-500'
/>
}
<div className={`${styles.agentIndicator}`}>
<a className='no-underline mx-2 flex text-muted-foreground' href={constructAgentLink()} target="_blank" rel="noreferrer">
<Lightbulb color='orange' weight='fill' />
<span>{constructAgentName()}</span>
</a>
</div> </div>
</div> </div>
</div> </div>
</ScrollArea>
) )
} }

View File

@@ -0,0 +1,4 @@
div.actualInputArea {
display: grid;
grid-template-columns: auto 1fr auto auto;
}

View File

@@ -0,0 +1,318 @@
import styles from './chatInputArea.module.css';
import React, { useEffect, useRef, useState } from 'react';
import { uploadDataForIndexing } from '../../common/chatFunctions';
import { Progress } from "@/components/ui/progress"
import 'katex/dist/katex.min.css';
import {
ArrowCircleUp,
ArrowRight,
Browser,
ChatsTeardrop,
FileArrowUp,
GlobeSimple,
Gps,
Image,
Microphone,
Notebook,
Question,
Robot,
Shapes
} from '@phosphor-icons/react';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command"
import { Textarea } from "@/components/ui/textarea"
import { Button } from '@/components/ui/button';
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog';
import { Popover, PopoverContent } from '@/components/ui/popover';
import { PopoverTrigger } from '@radix-ui/react-popover';
import Link from 'next/link';
import { AlertDialogCancel } from '@radix-ui/react-alert-dialog';
import LoginPrompt from '../loginPrompt/loginPrompt';
export interface ChatOptions {
[key: string]: string
}
interface ChatInputProps {
sendMessage: (message: string) => void;
sendDisabled: boolean;
setUploadedFiles?: (files: string[]) => void;
conversationId?: string | null;
chatOptionsData?: ChatOptions | null;
isMobileWidth?: boolean;
isLoggedIn: boolean;
}
export default function ChatInputArea(props: ChatInputProps) {
const [message, setMessage] = useState('');
const fileInputRef = useRef<HTMLInputElement>(null);
const [warning, setWarning] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const [loginRedirectMessage, setLoginRedirectMessage] = useState<string | null>(null);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const [progressValue, setProgressValue] = useState(0);
useEffect(() => {
if (!uploading) {
setProgressValue(0);
}
if (uploading) {
const interval = setInterval(() => {
setProgressValue((prev) => {
const increment = Math.floor(Math.random() * 5) + 1; // Generates a random number between 1 and 5
const nextValue = prev + increment;
return nextValue < 100 ? nextValue : 100; // Ensures progress does not exceed 100
});
}, 800);
return () => clearInterval(interval);
}
}, [uploading]);
function onSendMessage() {
if (!message.trim()) return;
if (!props.isLoggedIn) {
setLoginRedirectMessage('Hey there, you need to be signed in to send messages to Khoj AI');
setShowLoginPrompt(true);
return;
}
props.sendMessage(message.trim());
setMessage('');
}
function handleSlashCommandClick(command: string) {
setMessage(`/${command} `);
}
function handleFileButtonClick() {
if (!fileInputRef.current) return;
fileInputRef.current.click();
}
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
if (!event.target.files) return;
if (!props.isLoggedIn) {
setLoginRedirectMessage('Whoa! You need to login to upload files');
setShowLoginPrompt(true);
return;
}
uploadDataForIndexing(
event.target.files,
setWarning,
setUploading,
setError,
props.setUploadedFiles,
props.conversationId);
}
function getIconForSlashCommand(command: string) {
if (command.includes('summarize')) {
return <Gps className='h-4 w-4 mr-2' />
}
if (command.includes('help')) {
return <Question className='h-4 w-4 mr-2' />
}
if (command.includes('automation')) {
return <Robot className='h-4 w-4 mr-2' />
}
if (command.includes('webpage')) {
return <Browser className='h-4 w-4 mr-2' />
}
if (command.includes('notes')) {
return <Notebook className='h-4 w-4 mr-2' />
}
if (command.includes('image')) {
return <Image className='h-4 w-4 mr-2' />
}
if (command.includes('default')) {
return <Shapes className='h-4 w-4 mr-2' />
}
if (command.includes('general')) {
return <ChatsTeardrop className='h-4 w-4 mr-2' />
}
if (command.includes('online')) {
return <GlobeSimple className='h-4 w-4 mr-2' />
}
return <ArrowRight className='h-4 w-4 mr-2' />
}
return (
<>
{
showLoginPrompt && loginRedirectMessage && (
<LoginPrompt
onOpenChange={setShowLoginPrompt}
loginRedirectMessage={loginRedirectMessage} />
)
}
{
uploading && (
<AlertDialog
open={uploading}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Uploading data. Please wait.</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
<Progress
indicatorColor='bg-slate-500'
className='w-full h-2 rounded-full'
value={progressValue} />
</AlertDialogDescription>
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500' onClick={() => setUploading(false)}>Dismiss</AlertDialogAction>
</AlertDialogContent>
</AlertDialog>
)}
{
warning && (
<AlertDialog
open={warning !== null}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Data Upload Warning</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>{warning}</AlertDialogDescription>
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500' onClick={() => setWarning(null)}>Close</AlertDialogAction>
</AlertDialogContent>
</AlertDialog>
)
}
{
error && (
<AlertDialog
open={error !== null}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Oh no!</AlertDialogTitle>
<AlertDialogDescription>Something went wrong while uploading your data</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogDescription>{error}</AlertDialogDescription>
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500' onClick={() => setError(null)}>Close</AlertDialogAction>
</AlertDialogContent>
</AlertDialog>
)
}
{
(message.startsWith('/') && message.split(' ').length === 1) &&
<div className='flex justify-center text-center'>
<Popover
open={message.startsWith('/')}>
<PopoverTrigger className='flex justify-center text-center'>
</PopoverTrigger>
<PopoverContent
onOpenAutoFocus={(e) => e.preventDefault()}
className={`${props.isMobileWidth ? 'w-[100vw]' : 'w-full'} rounded-md`}>
<Command className='max-w-full'>
<CommandInput placeholder="Type a command or search..." value={message} className='hidden' />
<CommandList>
<CommandEmpty>No matching commands.</CommandEmpty>
<CommandGroup heading="Agent Tools">
{props.chatOptionsData && Object.entries(props.chatOptionsData).map(([key, value]) => (
<CommandItem
key={key}
className={`text-md`}
onSelect={() => handleSlashCommandClick(key)}>
<div
className='grid grid-cols-1 gap-1'>
<div
className='font-bold flex items-center'>
{getIconForSlashCommand(key)}
/{key}
</div>
<div>
{value}
</div>
</div>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
}
<div className={`${styles.actualInputArea} flex items-center justify-between`}>
<input
type="file"
multiple={true}
ref={fileInputRef}
onChange={handleFileChange}
style={{ display: 'none' }}
/>
<Button
variant={'ghost'}
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
disabled={props.sendDisabled}
onClick={handleFileButtonClick}>
<FileArrowUp weight='fill' />
</Button>
<div className="grid w-full gap-1.5 relative">
<Textarea
className='border-none min-h-[20px]'
placeholder="Type / to see a list of commands"
id="message"
value={message}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSendMessage();
}
}}
onChange={(e) => setMessage(e.target.value)}
disabled={props.sendDisabled} />
</div>
<Button
variant={'ghost'}
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
disabled={props.sendDisabled}>
<Microphone weight='fill' />
</Button>
<Button
className="bg-orange-300 hover:bg-orange-500 rounded-full p-0 h-auto text-3xl transition transform hover:-translate-y-1"
onClick={onSendMessage}
disabled={props.sendDisabled}>
<ArrowCircleUp />
</Button>
</div>
</>
)
}

View File

@@ -1,20 +1,46 @@
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 var(--box-shadow-color)
}
div.chatMessageWrapper {
padding-left: 24px;
}
div.khojfullHistory {
border-width: 1px;
padding-left: 4px;
}
div.youfullHistory {
max-width: 80%;
}
div.chatMessageContainer.youfullHistory {
padding-left: 0px;
} }
div.you { div.you {
color: var(--frosted-background-color); background-color: hsla(var(--secondary));
background-color: var(--intense-green);
align-self: flex-end; align-self: flex-end;
border-radius: 16px;
} }
div.khoj { div.khoj {
background-color: transparent; background-color: transparent;
color: #000000; color: hsl(var(--accent-foreground));
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%;
} }
@@ -23,8 +49,8 @@ div.chatMessageContainer h3 img {
width: 24px; width: 24px;
} }
div.you .author { div.you {
color: var(--frosted-background-color); color: hsla(var(--secondary-foreground));
} }
div.author { div.author {
@@ -36,31 +62,32 @@ div.author {
div.chatFooter { div.chatFooter {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-top: 8px; min-height: 28px;
} }
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(--secondary));
box-shadow: 0 4px 10px var(--box-shadow-color);
} }
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: hsla(var(--frosted-background-color));
color: var(--intense-green);
}
div.chatTimestamp {
font-size: small;
} }
button.codeCopyButton { button.codeCopyButton {
@@ -70,11 +97,35 @@ button.codeCopyButton {
} }
button.codeCopyButton:hover { button.codeCopyButton:hover {
background-color: var(--intense-green); color: hsla(var(--frosted-background-color));
color: var(--frosted-background-color);
} }
div.feedbackButtons img, div.feedbackButtons img,
button.codeCopyButton img,
button.copyButton img { button.copyButton img {
width: 24px; width: 24px;
} }
div.trainOfThought strong {
font-weight: 500;
}
div.trainOfThought.primary strong {
font-weight: 500;
color: hsla(var(--secondary-foreground));
}
div.trainOfThought.primary p {
color: inherit;
}
@media screen and (max-width: 768px) {
div.youfullHistory {
max-width: 100%;
}
div.chatMessageWrapper {
padding-left: 8px;
}
}

View File

@@ -5,12 +5,15 @@ import styles from './chatMessage.module.css';
import markdownIt from 'markdown-it'; import markdownIt from 'markdown-it';
import mditHljs from "markdown-it-highlightjs"; import mditHljs from "markdown-it-highlightjs";
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
import 'highlight.js/styles/github.css'
import { hasValidReferences } from '../referencePanel/referencePanel'; import { TeaserReferencesSection, constructAllReferences } from '../referencePanel/referencePanel';
import { ThumbsUp, ThumbsDown, Copy, Brain, Cloud, Folder, Book, Aperture, ArrowRight, SpeakerHifi } from '@phosphor-icons/react';
import { MagnifyingGlass } from '@phosphor-icons/react/dist/ssr';
import * as DomPurify from 'dompurify';
const md = new markdownIt({ const md = new markdownIt({
html: true, html: true,
@@ -77,23 +80,37 @@ interface AgentData {
interface Intent { interface Intent {
type: string; type: string;
query: string;
"memory-type": string;
"inferred-queries": string[]; "inferred-queries": string[];
} }
export interface SingleChatMessage { export interface SingleChatMessage {
automationId: string; automationId: string;
by: string; by: string;
intent: {
[key: string]: string
}
message: string; message: string;
context: Context[]; context: Context[];
created: string; created: string;
onlineContext: { onlineContext: {
[key: string]: OnlineContextData [key: string]: OnlineContextData
} }
rawQuery?: string;
intent?: Intent;
} }
export interface StreamMessage {
rawResponse: string;
trainOfThought: string[];
context: Context[];
onlineContext: {
[key: string]: OnlineContextData
}
completed: boolean;
rawQuery: string;
timestamp: string;
}
export interface ChatHistoryData { export interface ChatHistoryData {
chat: SingleChatMessage[]; chat: SingleChatMessage[];
agent: AgentData; agent: AgentData;
@@ -101,46 +118,97 @@ export interface ChatHistoryData {
slug: string; slug: string;
} }
function FeedbackButtons() { function sendFeedback(uquery: string, kquery: string, sentiment: string) {
fetch('/api/chat/feedback', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ uquery: uquery, kquery: kquery, sentiment: sentiment })
})
}
function FeedbackButtons({ uquery, kquery }: { uquery: string, kquery: string }) {
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} onClick={() => sendFeedback(uquery, kquery, 'positive')}>
<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} onClick={() => sendFeedback(uquery, kquery, 'negative')}>
<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) {
event.preventDefault();
setReferencePanelData(chatMessage);
setShowReferencePanel(true);
}
interface ChatMessageProps { interface ChatMessageProps {
chatMessage: SingleChatMessage; chatMessage: SingleChatMessage;
setReferencePanelData: Function; isMobileWidth: boolean;
setShowReferencePanel: Function; customClassName?: string;
borderLeftColor?: string;
}
interface TrainOfThoughtProps {
message: string;
primary: boolean;
}
function chooseIconFromHeader(header: string, iconColor: string) {
const compareHeader = header.toLowerCase();
if (compareHeader.includes("understanding")) {
return <Brain className={`inline mr-2 ${iconColor}`} />
}
if (compareHeader.includes("generating")) {
return <Cloud className={`inline mr-2 ${iconColor}`} />;
}
if (compareHeader.includes("data sources")) {
return <Folder className={`inline mr-2 ${iconColor}`} />;
}
if (compareHeader.includes("notes")) {
return <Folder className={`inline mr-2 ${iconColor}`} />;
}
if (compareHeader.includes("read")) {
return <Book className={`inline mr-2 ${iconColor}`} />;
}
if (compareHeader.includes("search")) {
return <MagnifyingGlass className={`inline mr-2 ${iconColor}`} />;
}
if (compareHeader.includes("summary") || compareHeader.includes("summarize")) {
return <Aperture className={`inline mr-2 ${iconColor}`} />;
}
return <Brain className={`inline mr-2 ${iconColor}`} />;
}
export function TrainOfThought(props: TrainOfThoughtProps) {
// The train of thought comes in as a markdown-formatted string. It starts with a heading delimited by two asterisks at the start and end and a colon, followed by the message. Example: **header**: status. This function will parse the message and render it as a div.
let extractedHeader = props.message.match(/\*\*(.*)\*\*/);
let header = extractedHeader ? extractedHeader[1] : "";
const iconColor = props.primary ? 'text-orange-400' : 'text-gray-500';
const icon = chooseIconFromHeader(header, iconColor);
let markdownRendered = DomPurify.sanitize(md.render(props.message));
return (
<div className={`flex items-center ${props.primary ? 'text-gray-400' : 'text-gray-300'} ${styles.trainOfThought} ${props.primary ? styles.primary : ''}`} >
{icon}
<div dangerouslySetInnerHTML={{ __html: markdownRendered }} />
</div>
)
} }
export default function ChatMessage(props: ChatMessageProps) { export default function ChatMessage(props: ChatMessageProps) {
const [copySuccess, setCopySuccess] = useState<boolean>(false); const [copySuccess, setCopySuccess] = useState<boolean>(false);
const [isHovering, setIsHovering] = useState<boolean>(false);
const [markdownRendered, setMarkdownRendered] = useState<string>('');
const messageRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let message = props.chatMessage.message; let message = props.chatMessage.message;
// Replace LaTeX delimiters with placeholders // Replace LaTeX delimiters with placeholders
@@ -156,8 +224,16 @@ export default function ChatMessage(props: ChatMessageProps) {
// Replace placeholders with LaTeX delimiters // Replace placeholders with LaTeX delimiters
markdownRendered = markdownRendered.replace(/LEFTPAREN/g, '\\(').replace(/RIGHTPAREN/g, '\\)') markdownRendered = markdownRendered.replace(/LEFTPAREN/g, '\\(').replace(/RIGHTPAREN/g, '\\)')
.replace(/LEFTBRACKET/g, '\\[').replace(/RIGHTBRACKET/g, '\\]'); .replace(/LEFTBRACKET/g, '\\[').replace(/RIGHTBRACKET/g, '\\]');
setMarkdownRendered(DomPurify.sanitize(markdownRendered));
}, [props.chatMessage.message]);
const messageRef = useRef<HTMLDivElement>(null); useEffect(() => {
if (copySuccess) {
setTimeout(() => {
setCopySuccess(false);
}, 2000);
}
}, [copySuccess]);
useEffect(() => { useEffect(() => {
if (messageRef.current) { if (messageRef.current) {
@@ -165,7 +241,7 @@ export default function ChatMessage(props: ChatMessageProps) {
preElements.forEach((preElement) => { preElements.forEach((preElement) => {
const copyButton = document.createElement('button'); const copyButton = document.createElement('button');
const copyImage = document.createElement('img'); const copyImage = document.createElement('img');
copyImage.src = '/copy-button.svg'; copyImage.src = '/static/copy-button.svg';
copyImage.alt = 'Copy'; copyImage.alt = 'Copy';
copyImage.width = 24; copyImage.width = 24;
copyImage.height = 24; copyImage.height = 24;
@@ -179,52 +255,97 @@ export default function ChatMessage(props: ChatMessageProps) {
textContent = textContent.replace(/^Copy/, ''); textContent = textContent.replace(/^Copy/, '');
textContent = textContent.trim(); textContent = textContent.trim();
navigator.clipboard.writeText(textContent); navigator.clipboard.writeText(textContent);
copyImage.src = '/static/copy-button-success.svg';
}); });
preElement.prepend(copyButton); preElement.prepend(copyButton);
}); });
} }
}, [markdownRendered]); }, [markdownRendered, isHovering, messageRef]);
if (!props.chatMessage.message) {
return null;
}
function renderTimeStamp(timestamp: string) { function renderTimeStamp(timestamp: string) {
var dateObject = new Date(timestamp); if (!timestamp.endsWith('Z')) {
var month = dateObject.getMonth() + 1; timestamp = timestamp + 'Z';
var date = dateObject.getDate(); }
var year = dateObject.getFullYear(); const messageDateTime = new Date(timestamp);
const formattedDate = `${month}/${date}/${year}`; const currentDataTime = new Date();
return `${formattedDate} ${dateObject.toLocaleTimeString()}`; const timeDiff = currentDataTime.getTime() - messageDateTime.getTime();
if (timeDiff < 60000) {
return "Just now";
} }
useEffect(() => { if (timeDiff < 3600000) {
if (copySuccess) { // Using Math.round for closer to actual time representation
setTimeout(() => { return `${Math.round(timeDiff / 60000)}m ago`;
setCopySuccess(false);
}, 2000);
} }
}, [copySuccess]);
let referencesValid = hasValidReferences(props.chatMessage); if (timeDiff < 86400000) {
return `${Math.round(timeDiff / 3600000)}h ago`;
}
return `${Math.round(timeDiff / 86400000)}d ago`;
}
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") {
const dynamicBorderColor = `border-l-${props.borderLeftColor}`;
classes.push(`border-l-4 border-opacity-50 border-l-orange-400 ${dynamicBorderColor}`);
}
return classes.join(' ');
}
const allReferences = constructAllReferences(props.chatMessage.context, props.chatMessage.onlineContext);
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}> onMouseLeave={(event) => setIsHovering(false)}
{/* <div className={styles.chatFooter}> */} onMouseEnter={(event) => setIsHovering(true)}
{/* {props.chatMessage.by} */} onClick={props.chatMessage.by === "khoj" ? (event) => undefined : undefined}>
{/* </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 */} </div>
<div className={styles.teaserReferencesContainer}>
<TeaserReferencesSection
isMobileWidth={props.isMobileWidth}
notesReferenceCardData={allReferences.notesReferenceCardData}
onlineReferenceCardData={allReferences.onlineReferenceCardData} />
</div>
<div className={styles.chatFooter}> <div className={styles.chatFooter}>
<div className={styles.chatTimestamp}> {
isHovering &&
(
<>
<div className={`text-gray-400 relative top-2 left-2`}>
{renderTimeStamp(props.chatMessage.created)} {renderTimeStamp(props.chatMessage.created)}
</div> </div>
<div className={styles.chatButtons}> <div className={styles.chatButtons}>
{ {
referencesValid && (props.chatMessage.by === "khoj") &&
<div className={styles.referenceButton}> (
<button onClick={(event) => onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel)}> <button onClick={(event) => console.log("speaker")}>
References <SpeakerHifi color='hsl(var(--muted-foreground))' />
</button> </button>
</div> )
} }
<button className={`${styles.copyButton}`} onClick={() => { <button className={`${styles.copyButton}`} onClick={() => {
navigator.clipboard.writeText(props.chatMessage.message); navigator.clipboard.writeText(props.chatMessage.message);
@@ -232,26 +353,27 @@ 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>
{ {
props.chatMessage.by === "khoj" && <FeedbackButtons /> (props.chatMessage.by === "khoj") &&
(
props.chatMessage.intent ?
<FeedbackButtons
uquery={props.chatMessage.intent.query}
kquery={props.chatMessage.message} />
: <FeedbackButtons
uquery={props.chatMessage.rawQuery || props.chatMessage.message}
kquery={props.chatMessage.message} />
)
} }
</div> </div>
</>
)
}
</div> </div>
</div> </div>
) )

View File

@@ -0,0 +1,22 @@
import { CircleNotch } from '@phosphor-icons/react';
export default function Loading() {
return (
// NOTE: We can display usage tips here for casual learning moments.
<div className={`bg-background opacity-50 flex items-center justify-center h-screen`}>
<div>Loading <span><CircleNotch className={`inline animate-spin h-5 w-5`} /></span></div>
</div>
);
}
interface InlineLoadingProps {
className?: string;
}
export function InlineLoading(props: InlineLoadingProps) {
return (
<button className={`${props.className}`}>
<CircleNotch className={`animate-spin h-5 w-5 mr-3`} />
</button>
)
}

View File

@@ -0,0 +1,42 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog';
import Link from 'next/link';
export interface LoginPromptProps {
loginRedirectMessage: string;
onOpenChange: (open: boolean) => void;
}
export default function LoginPrompt(props: LoginPromptProps) {
return (
<AlertDialog
open={true}
onOpenChange={props.onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Sign in to Khoj to continue</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>{props.loginRedirectMessage}. By logging in, you agree to our <Link href="https://khoj.dev/terms-of-service">Terms of Service.</Link></AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Dismiss</AlertDialogCancel>
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500'
onClick={() => {
window.location.href = `/login?next=${encodeURIComponent(window.location.pathname)}`;
}}>
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`}> {/* Redirect to login page */}
Login
</Link>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

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,164 @@
'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";
import { Toggle } from '@/components/ui/toggle';
import { Moon } from '@phosphor-icons/react';
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);
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
setIsMobileWidth(window.innerWidth < 768);
setDisplayTitle(props.title || props.selected.toUpperCase());
}, [props.title]);
useEffect(() => {
window.addEventListener('resize', () => {
setIsMobileWidth(window.innerWidth < 768);
});
if (localStorage.getItem('theme') === 'dark') {
document.documentElement.classList.add('dark');
setDarkMode(true);
}
}, []);
useEffect(() => {
toggleDarkMode(darkMode);
}, [darkMode]);
function toggleDarkMode(darkMode: boolean) {
if (darkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
localStorage.setItem('theme', darkMode ? 'dark' : 'light');
}
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 inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground'>
<MenubarMenu>
<Link href='/chat' target="_blank" rel="noreferrer" className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>
<MenubarTrigger>Chat</MenubarTrigger>
</Link>
</MenubarMenu>
<MenubarMenu>
<Link href='/agents' target="_blank" rel="noreferrer" className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>
<MenubarTrigger>Agents</MenubarTrigger>
</Link>
</MenubarMenu>
<MenubarMenu>
<Link href='/automations' target="_blank" rel="noreferrer" className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>
<MenubarTrigger>Automations</MenubarTrigger>
</Link>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Profile</MenubarTrigger>
<MenubarContent>
<MenubarItem>
<Toggle
pressed={darkMode}
onClick={() => {
setDarkMode(!darkMode)
}
}>
<Moon />
</Toggle>
</MenubarItem>
{userData &&
<>
<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>
)} </>
</div> }
); </MenubarContent>
} </MenubarMenu>
export default function NavMenu(props: NavMenuProps) { </Menubar>
}
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

@@ -1,31 +0,0 @@
div.panel {
padding: 1rem;
border-radius: 1rem;
background-color: var(--calm-blue);
max-height: 80vh;
overflow-y: auto;
max-width: auto;
}
div.panel a {
color: var(--intense-green);
text-decoration: underline;
}
div.onlineReference,
div.contextReference {
margin: 4px;
border-radius: 8px;
padding: 4px;
}
div.contextReference:hover {
cursor: pointer;
}
div.singleReference {
padding: 8px;
border-radius: 8px;
background-color: var(--frosted-background-color);
margin-top: 8px;
}

View File

@@ -1,8 +1,8 @@
'use client' 'use client'
import styles from "./referencePanel.module.css"; import { useEffect, useState } from "react";
import { useState } from "react"; import { ArrowRight, File } from "@phosphor-icons/react";
import markdownIt from "markdown-it"; import markdownIt from "markdown-it";
const md = new markdownIt({ const md = new markdownIt({
@@ -11,183 +11,329 @@ const md = new markdownIt({
typographer: true typographer: true
}); });
import { SingleChatMessage, Context, WebPage, OnlineContextData } from "../chatMessage/chatMessage"; import { Context, WebPage, OnlineContextData } from "../chatMessage/chatMessage";
import { Card } from "@/components/ui/card";
interface ReferencePanelProps { import {
referencePanelData: SingleChatMessage | null; Sheet,
setShowReferencePanel: (showReferencePanel: boolean) => void; SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import * as DomPurify from 'dompurify';
interface NotesContextReferenceData {
title: string;
content: string;
} }
export function hasValidReferences(referencePanelData: SingleChatMessage | null) { interface NotesContextReferenceCardProps extends NotesContextReferenceData {
showFullContent: boolean;
}
function NotesContextReferenceCard(props: NotesContextReferenceCardProps) {
const snippet = props.showFullContent ? DomPurify.sanitize(md.render(props.content)) : DomPurify.sanitize(props.content);
const [isHovering, setIsHovering] = useState(false);
return ( return (
referencePanelData && <>
( <Popover
(referencePanelData.context && referencePanelData.context.length > 0) || open={isHovering && !props.showFullContent}
(referencePanelData.onlineContext && Object.keys(referencePanelData.onlineContext).length > 0 && onOpenChange={setIsHovering}
Object.values(referencePanelData.onlineContext).some( >
(onlineContextData) => <PopoverTrigger asChild>
(onlineContextData.webpages && onlineContextData.webpages.length > 0)|| onlineContextData.answerBox || onlineContextData.peopleAlsoAsk || onlineContextData.knowledgeGraph)) <Card
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
className={`${props.showFullContent ? 'w-auto' : 'w-[200px]'} overflow-hidden break-words text-balance rounded-lg p-2 bg-muted border-none`}
>
<h3 className={`${props.showFullContent ? 'block' : 'line-clamp-1'} text-muted-foreground}`}>
<File className='w-6 h-6 text-muted-foreground inline-flex' />
{props.title}
</h3>
<p className={`${props.showFullContent ? 'block' : 'overflow-hidden line-clamp-2'}`} dangerouslySetInnerHTML={{ __html: snippet }}></p>
</Card>
</PopoverTrigger>
<PopoverContent
className="w-[400px] mx-2">
<Card className={`w-auto overflow-hidden break-words text-balance rounded-lg p-2 border-none`}>
<h3 className={`line-clamp-2 text-muted-foreground}`}>
<File className='w-6 h-6 text-muted-foreground inline-flex' />
{props.title}
</h3>
<p className={`overflow-hidden line-clamp-3`} dangerouslySetInnerHTML={{ __html: snippet }}></p>
</Card>
</PopoverContent>
</Popover>
</>
) )
);
} }
function CompiledReference(props: { context: (Context | string) }) { export interface ReferencePanelData {
notesReferenceCardData: NotesContextReferenceData[];
onlineReferenceCardData: OnlineReferenceData[];
}
let snippet = "";
let file = ""; interface OnlineReferenceData {
if (typeof props.context === "string") { title: string;
// Treat context as a string and get the first line for the file name description: string;
const lines = props.context.split("\n"); link: string;
file = lines[0]; }
snippet = lines.slice(1).join("\n");
interface OnlineReferenceCardProps extends OnlineReferenceData {
showFullContent: boolean;
}
function GenericOnlineReferenceCard(props: OnlineReferenceCardProps) {
const [isHovering, setIsHovering] = useState(false);
if (!props.link) {
return null;
}
const domain = new URL(props.link).hostname;
const favicon = `https://www.google.com/s2/favicons?domain=${domain}`;
const handleMouseEnter = () => {
setIsHovering(true);
}
const handleMouseLeave = () => {
setIsHovering(false);
}
return (
<>
<Popover
open={isHovering && !props.showFullContent}
onOpenChange={setIsHovering}
>
<PopoverTrigger asChild>
<Card
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`${props.showFullContent ? 'w-auto' : 'w-[200px]'} overflow-hidden break-words rounded-lg text-balance p-2 bg-muted border-none`}>
<div className='flex flex-col'>
<a href={props.link} target="_blank" rel="noreferrer" className='!no-underline p-2'>
<div className='flex items-center'>
<img src={favicon} alt="" className='!w-4 h-4 mr-2' />
<h3 className={`overflow-hidden ${props.showFullContent ? 'block' : 'line-clamp-1'} text-muted-foreground`}>{domain}</h3>
</div>
<h3 className={`overflow-hidden ${props.showFullContent ? 'block' : 'line-clamp-1'} font-bold`}>{props.title}</h3>
<p className={`overflow-hidden ${props.showFullContent ? 'block' : 'line-clamp-2'}`}>{props.description}</p>
</a>
</div>
</Card>
</PopoverTrigger>
<PopoverContent
className="w-[400px] mx-2">
<Card
className={`w-auto overflow-hidden break-words text-balance rounded-lg border-none`}>
<div className='flex flex-col'>
<a href={props.link} target="_blank" rel="noreferrer" className='!no-underline p-2'>
<div className='flex items-center'>
<img src={favicon} alt="" className='!w-4 h-4 mr-2' />
<h3 className={`overflow-hidden ${props.showFullContent ? 'block' : 'line-clamp-2'} text-muted-foreground`}>{domain}</h3>
</div>
<h3 className={`overflow-hidden ${props.showFullContent ? 'block' : 'line-clamp-2'} font-bold`}>{props.title}</h3>
<p className={`overflow-hidden ${props.showFullContent ? 'block' : 'line-clamp-3'}`}>{props.description}</p>
</a>
</div>
</Card>
</PopoverContent>
</Popover>
</>
)
}
export function constructAllReferences(contextData: Context[], onlineData: { [key: string]: OnlineContextData }) {
const onlineReferences: OnlineReferenceData[] = [];
const contextReferences: NotesContextReferenceData[] = [];
if (onlineData) {
let localOnlineReferences = [];
for (const [key, value] of Object.entries(onlineData)) {
if (value.answerBox) {
localOnlineReferences.push({
title: value.answerBox.title,
description: value.answerBox.answer,
link: value.answerBox.source
});
}
if (value.knowledgeGraph) {
localOnlineReferences.push({
title: value.knowledgeGraph.title,
description: value.knowledgeGraph.description,
link: value.knowledgeGraph.descriptionLink
});
}
if (value.webpages) {
// If webpages is of type Array, iterate through it and add each webpage to the localOnlineReferences array
if (value.webpages instanceof Array) {
let webPageResults = value.webpages.map((webPage) => {
return {
title: webPage.query,
description: webPage.snippet,
link: webPage.link
}
});
localOnlineReferences.push(...webPageResults);
} else { } else {
const context = props.context as Context; let singleWebpage = value.webpages as WebPage;
snippet = context.compiled;
file = context.file; // If webpages is an object, add the object to the localOnlineReferences array
localOnlineReferences.push({
title: singleWebpage.query,
description: singleWebpage.snippet,
link: singleWebpage.link
});
}
} }
const [showSnippet, setShowSnippet] = useState(false); if (value.organic) {
let organicResults = value.organic.map((organicContext) => {
return {
title: organicContext.title,
description: organicContext.snippet,
link: organicContext.link
}
});
return ( localOnlineReferences.push(...organicResults);
<div className={styles.singleReference}> }
<div className={styles.contextReference} onClick={() => setShowSnippet(!showSnippet)}> }
<div className={styles.referencePanelTitle}>
{file} onlineReferences.push(...localOnlineReferences);
</div> }
<div className={styles.referencePanelContent} style={{ display: showSnippet ? "block" : "none" }}>
<div> if (contextData) {
{snippet}
</div> let localContextReferences = contextData.map((context) => {
</div> if (!context.compiled) {
</div> const fileContent = context as unknown as string;
</div> const title = fileContent.split('\n')[0];
) const content = fileContent.split('\n').slice(1).join('\n');
return {
title: title,
content: content
};
}
return {
title: context.file,
content: context.compiled
}
});
contextReferences.push(...localContextReferences);
}
return {
notesReferenceCardData: contextReferences,
onlineReferenceCardData: onlineReferences
}
} }
function WebPageReference(props: { webpages: WebPage, query: string | null }) {
let snippet = md.render(props.webpages.snippet); export interface TeaserReferenceSectionProps {
notesReferenceCardData: NotesContextReferenceData[];
const [showSnippet, setShowSnippet] = useState(false); onlineReferenceCardData: OnlineReferenceData[];
isMobileWidth: boolean;
return (
<div className={styles.onlineReference} onClick={() => setShowSnippet(!showSnippet)}>
<div className={styles.onlineReferenceTitle}>
<a href={props.webpages.link} target="_blank" rel="noreferrer">
{
props.query ? (
<span>
{props.query}
</span>
) : <span>
{props.webpages.query}
</span>
}
</a>
</div>
<div className={styles.onlineReferenceContent} style={{ display: showSnippet ? "block" : "none" }}>
<div dangerouslySetInnerHTML={{ __html: snippet }}></div>
</div>
</div>
)
} }
function OnlineReferences(props: { onlineContext: OnlineContextData, query: string}) { export function TeaserReferencesSection(props: TeaserReferenceSectionProps) {
const [numTeaserSlots, setNumTeaserSlots] = useState(3);
const webpages = props.onlineContext.webpages; useEffect(() => {
const answerBox = props.onlineContext.answerBox; setNumTeaserSlots(props.isMobileWidth ? 1 : 3);
const peopleAlsoAsk = props.onlineContext.peopleAlsoAsk; }, [props.isMobileWidth]);
const knowledgeGraph = props.onlineContext.knowledgeGraph;
return ( const notesDataToShow = props.notesReferenceCardData.slice(0, numTeaserSlots);
<div className={styles.singleReference}> const onlineDataToShow = notesDataToShow.length < numTeaserSlots ? props.onlineReferenceCardData.slice(0, numTeaserSlots - notesDataToShow.length) : [];
{
webpages && (
!Array.isArray(webpages) ? (
<WebPageReference webpages={webpages} query={props.query} />
) : (
webpages.map((webpage, index) => {
return <WebPageReference webpages={webpage} key={index} query={null} />
})
)
)
}
{
answerBox && (
<div className={styles.onlineReference}>
<div className={styles.onlineReferenceTitle}>
{answerBox.title}
</div>
<div className={styles.onlineReferenceContent}>
<div>
{answerBox.answer}
</div>
</div>
</div>
)
}
{
peopleAlsoAsk && peopleAlsoAsk.map((people, index) => {
return (
<div className={styles.onlineReference} key={index}>
<div className={styles.onlineReferenceTitle}>
<a href={people.link} target="_blank" rel="noreferrer">
{people.question}
</a>
</div>
<div className={styles.onlineReferenceContent}>
<div>
{people.snippet}
</div>
</div>
</div>
)
})
}
{
knowledgeGraph && (
<div className={styles.onlineReference}>
<div className={styles.onlineReferenceTitle}>
<a href={knowledgeGraph.descriptionLink} target="_blank" rel="noreferrer">
{knowledgeGraph.title}
</a>
</div>
<div className={styles.onlineReferenceContent}>
<div>
{knowledgeGraph.description}
</div>
</div>
</div>
)
}
</div>
) const shouldShowShowMoreButton = props.notesReferenceCardData.length > 0 || props.onlineReferenceCardData.length > 0;
}
export default function ReferencePanel(props: ReferencePanelProps) { const numReferences = props.notesReferenceCardData.length + props.onlineReferenceCardData.length;
if (!props.referencePanelData) { if (numReferences === 0) {
return null;
}
if (!hasValidReferences(props.referencePanelData)) {
return null; return null;
} }
return ( return (
<div className={`${styles.panel}`}> <div className={`${props.isMobileWidth ? 'p-0' : 'p-4'}`}>
References <button onClick={() => props.setShowReferencePanel(false)}>Hide</button> <h3 className="inline-flex items-center">
References
<p className="text-gray-400 m-2">
{numReferences} sources
</p>
</h3>
<div className={`flex ${props.isMobileWidth ? 'w-[90vw]' : 'w-auto'} space-x-4 mt-2`}>
{ {
props.referencePanelData?.context.map((context, index) => { notesDataToShow.map((note, index) => {
return <CompiledReference context={context} key={index} /> return <NotesContextReferenceCard showFullContent={false} {...note} key={`${note.title}-${index}`} />
}) })
} }
{ {
Object.entries(props.referencePanelData?.onlineContext || {}).map(([key, onlineContextData], index) => { onlineDataToShow.map((online, index) => {
return <OnlineReferences onlineContext={onlineContextData} query={key} key={index} /> return <GenericOnlineReferenceCard showFullContent={false} {...online} key={`${online.title}-${index}`} />
})
}
{
shouldShowShowMoreButton &&
<ReferencePanel
notesReferenceCardData={props.notesReferenceCardData}
onlineReferenceCardData={props.onlineReferenceCardData} />
}
</div>
</div>
)
}
interface ReferencePanelDataProps {
notesReferenceCardData: NotesContextReferenceData[];
onlineReferenceCardData: OnlineReferenceData[];
}
export default function ReferencePanel(props: ReferencePanelDataProps) {
if (!props.notesReferenceCardData && !props.onlineReferenceCardData) {
return null;
}
return (
<Sheet>
<SheetTrigger
className='text-balance w-[200px] overflow-hidden break-words p-0 bg-transparent border-none text-gray-400 align-middle justify-center items-center !m-0 inline-flex'>
View references <ArrowRight className='m-1' />
</SheetTrigger>
<SheetContent className="overflow-y-scroll">
<SheetHeader>
<SheetTitle>References</SheetTitle>
<SheetDescription>View all references for this response</SheetDescription>
</SheetHeader>
<div className="flex flex-col w-auto gap-2 mt-2">
{
props.notesReferenceCardData.map((note, index) => {
return <NotesContextReferenceCard showFullContent={true} {...note} key={`${note.title}-${index}`} />
})
}
{
props.onlineReferenceCardData.map((online, index) => {
return <GenericOnlineReferenceCard showFullContent={true} {...online} key={`${online.title}-${index}`} />
}) })
} }
</div> </div>
</SheetContent>
</Sheet>
); );
} }

View File

@@ -2,150 +2,787 @@
import styles from "./sidePanel.module.css"; import styles from "./sidePanel.module.css";
import { useEffect, useState } from "react"; import { Suspense, useEffect, useState } from "react";
import { UserProfile } from "@/app/common/auth"; import { UserProfile, useAuthenticatedData } 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 Image from "next/image";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { InlineLoading } from "../loading/loading";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ArrowRight, ArrowLeft, ArrowDown, Spinner, Check, FolderPlus, DotsThreeVertical, House, StackPlus, UserCirclePlus } from "@phosphor-icons/react";
interface ChatHistory { interface ChatHistory {
conversation_id: string; conversation_id: string;
slug: string; slug: string;
agent_name: string;
agent_avatar: string;
compressed: boolean;
created: string;
} }
function ChatSession(prop: ChatHistory) { import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Pencil, Trash, Share } from "@phosphor-icons/react";
import { Button, buttonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
import { modifyFileFilterForConversation } from "@/app/common/chatFunctions";
import { ScrollAreaScrollbar } from "@radix-ui/react-scroll-area";
// Define a fetcher function
const fetcher = (url: string) => fetch(url).then((res) => res.json());
interface GroupedChatHistory {
[key: string]: ChatHistory[];
}
function renameConversation(conversationId: string, newTitle: string) {
const editUrl = `/api/chat/title?client=web&conversation_id=${conversationId}&title=${newTitle}`;
fetch(editUrl, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
})
.catch(err => {
console.error(err);
return;
});
}
function shareConversation(conversationId: string, setShareUrl: (url: string) => void) {
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 => {
setShareUrl(data.url);
})
.catch(err => {
console.error(err);
return;
});
}
function deleteConversation(conversationId: string) {
const deleteUrl = `/api/chat/history?client=web&conversation_id=${conversationId}`;
fetch(deleteUrl, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
})
.catch(err => {
console.error(err);
return;
});
}
interface FilesMenuProps {
conversationId: string | null;
uploadedFiles: string[];
isMobileWidth: boolean;
}
function FilesMenu(props: FilesMenuProps) {
// Use SWR to fetch files
const { data: files, error } = useSWR<string[]>(props.conversationId ? '/api/config/data/computer' : null, fetcher);
const { data: selectedFiles, error: selectedFilesError } = useSWR(props.conversationId ? `/api/chat/conversation/file-filters/${props.conversationId}` : null, fetcher);
const [isOpen, setIsOpen] = useState(false);
const [unfilteredFiles, setUnfilteredFiles] = useState<string[]>([]);
const [addedFiles, setAddedFiles] = useState<string[]>([]);
useEffect(() => {
if (!files) return;
// First, sort lexically
files.sort();
let sortedFiles = files;
if (addedFiles) {
console.log("addedFiles in useeffect hook", addedFiles);
sortedFiles = addedFiles.concat(sortedFiles.filter((filename: string) => !addedFiles.includes(filename)));
}
setUnfilteredFiles(sortedFiles);
}, [files, addedFiles]);
useEffect(() => {
for (const file of props.uploadedFiles) {
setAddedFiles((addedFiles) => [...addedFiles, file]);
}
}, [props.uploadedFiles]);
useEffect(() => {
if (selectedFiles) {
setAddedFiles(selectedFiles);
}
}, [selectedFiles]);
const removeAllFiles = () => {
modifyFileFilterForConversation(props.conversationId, addedFiles, setAddedFiles, 'remove');
}
const addAllFiles = () => {
modifyFileFilterForConversation(props.conversationId, unfilteredFiles, setAddedFiles, 'add');
}
if (!props.conversationId) return (<></>);
if (error) return <div>Failed to load files</div>;
if (selectedFilesError) return <div>Failed to load selected files</div>;
if (!files) return <InlineLoading />;
if (!selectedFiles) return <InlineLoading />;
const FilesMenuCommandBox = () => {
return ( return (
<div key={prop.conversation_id} className={styles.session}> <Command>
<Link href={`/chat?conversationId=${prop.conversation_id}`}> <CommandInput placeholder="Find file" />
<p className={styles.session}>{prop.slug || "New Conversation 🌱"}</p> <CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Quick">
<CommandItem
onSelect={() => {
removeAllFiles();
}}
>
<Trash className="h-4 w-4 mr-2" />
<span>Clear all</span>
</CommandItem>
<CommandItem
onSelect={() => {
addAllFiles();
}}
>
<FolderPlus className="h-4 w-4 mr-2" />
<span>Select all</span>
</CommandItem>
</CommandGroup>
<CommandGroup heading="Configure files">
{unfilteredFiles.map((filename: string) => (
addedFiles && addedFiles.includes(filename) ?
<CommandItem
key={filename}
value={filename}
className="bg-accent text-accent-foreground mb-1"
onSelect={(value) => {
modifyFileFilterForConversation(props.conversationId, [value], setAddedFiles, 'remove');
}}
>
<Check className="h-4 w-4 mr-2" />
<span className="break-all">{filename}</span>
</CommandItem>
:
<CommandItem
key={filename}
className="mb-1"
value={filename}
onSelect={(value) => {
modifyFileFilterForConversation(props.conversationId, [value], setAddedFiles, 'add');
}}
>
<span className="break-all">{filename}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
);
}
if (props.isMobileWidth) {
return (
<>
<Drawer>
<DrawerTrigger className="bg-background border border-muted p-4 rounded-2xl my-8 text-left inline-flex items-center justify-between w-full">
Manage Files <ArrowRight className="h-4 w-4 mx-2" />
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Files</DrawerTitle>
<DrawerDescription>Manage files for this conversation</DrawerDescription>
</DrawerHeader>
<div className={`${styles.panelWrapper}`}>
<FilesMenuCommandBox />
</div>
<DrawerFooter>
<DrawerClose>
<Button variant="outline">Done</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
</>
);
}
return (
<>
<Popover
open={isOpen}
onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<div
className="w-auto bg-background border border-muted p-4 drop-shadow-sm rounded-2xl my-8">
<div className="flex items-center justify-between space-x-4">
<h4 className="text-sm font-semibold">
Manage Context
<p>
<span className="text-muted-foreground text-xs">Using {addedFiles.length == 0 ? files.length : addedFiles.length} files</span>
</p>
</h4>
<Button variant="ghost" size="sm" className="w-9 p-0">
{
isOpen ?
<ArrowDown className="h-4 w-4" />
:
<ArrowRight className="h-4 w-4" />
}
<span className="sr-only">Toggle</span>
</Button>
</div>
</div>
</PopoverTrigger>
<PopoverContent className={`mx-2`}>
<FilesMenuCommandBox />
</PopoverContent>
</Popover>
</>
)
}
interface SessionsAndFilesProps {
webSocketConnected?: boolean;
setEnabled: (enabled: boolean) => void;
subsetOrganizedData: GroupedChatHistory | null;
organizedData: GroupedChatHistory | null;
data: ChatHistory[] | null;
userProfile: UserProfile | null;
conversationId: string | null;
uploadedFiles: string[];
isMobileWidth: boolean;
}
function SessionsAndFiles(props: SessionsAndFilesProps) {
return (
<>
<ScrollArea className="h-[40vh]">
<ScrollAreaScrollbar orientation="vertical" className="h-full w-2.5 border-l border-l-transparent p-[1px]" />
<div className={styles.sessionsList}>
{props.subsetOrganizedData != null && Object.keys(props.subsetOrganizedData).map((timeGrouping) => (
<div key={timeGrouping} className={`my-4`}>
<div className={`text-muted-foreground text-sm font-bold p-[0.5rem] `}>
{timeGrouping}
</div>
{props.subsetOrganizedData && props.subsetOrganizedData[timeGrouping].map((chatHistory) => (
<ChatSession
created={chatHistory.created}
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>
{
(props.data && props.data.length > 5) && (
<ChatSessionsModal data={props.organizedData} />
)
}
<FilesMenu conversationId={props.conversationId} uploadedFiles={props.uploadedFiles} isMobileWidth={props.isMobileWidth} />
{props.userProfile &&
<UserProfileComponent userProfile={props.userProfile} webSocketConnected={props.webSocketConnected} collapsed={false} />
}</>
)
}
interface ChatSessionActionMenuProps {
conversationId: string;
}
function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
const [renamedTitle, setRenamedTitle] = useState('');
const [isRenaming, setIsRenaming] = useState(false);
const [isSharing, setIsSharing] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [shareUrl, setShareUrl] = useState('');
const [showShareUrl, setShowShareUrl] = useState(false);
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (isSharing) {
shareConversation(props.conversationId, setShareUrl);
setShowShareUrl(true);
setIsSharing(false);
}
}, [isSharing]);
if (isRenaming) {
return (
<Dialog
open={isRenaming}
onOpenChange={(open) => setIsRenaming(open)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Set a new title for the conversation</DialogTitle>
<DialogDescription>
This will help you identify the conversation easily, and also help you search for it later.
</DialogDescription>
<Input
value={renamedTitle}
onChange={(e) => setRenamedTitle(e.target.value)}
/>
</DialogHeader>
<DialogFooter>
<Button
onClick={() => {
renameConversation(props.conversationId, renamedTitle);
setIsRenaming(false);
}}
type="submit">Rename</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
if (isSharing || showShareUrl) {
if (shareUrl) {
navigator.clipboard.writeText(shareUrl);
}
return (
<Dialog
open={isSharing || showShareUrl}
onOpenChange={(open) => {
setShowShareUrl(open)
setIsSharing(open)
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>Conversation Share URL</DialogTitle>
<DialogDescription>
Sharing this chat session will allow anyone with a link to view the conversation.
<Input
className="w-full bg-accent text-accent-foreground rounded-md p-2 mt-2"
value={shareUrl}
readOnly={true}
/>
</DialogDescription>
</DialogHeader>
<DialogFooter>
{
!showShareUrl &&
<Button
onClick={() => {
shareConversation(props.conversationId, setShareUrl);
setShowShareUrl(true);
}}
className="bg-orange-500"
disabled><Spinner className="mr-2 h-4 w-4 animate-spin" />Sharing</Button>
}
{
showShareUrl &&
<Button
onClick={() => {
navigator.clipboard.writeText(shareUrl);
}}
variant={'default'}>Copy</Button>
}
</DialogFooter>
</DialogContent>
</Dialog>
)
}
if (isDeleting) {
return (
<AlertDialog
open={isDeleting}
onOpenChange={(open) => setIsDeleting(open)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Conversation</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this conversation? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
deleteConversation(props.conversationId);
setIsDeleting(false);
}}
className="bg-rose-500 hover:bg-rose-600">Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
return (
<DropdownMenu
onOpenChange={(open) => setIsOpen(open)}
open={isOpen}>
<DropdownMenuTrigger><DotsThreeVertical className="h-4 w-4" /></DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => setIsRenaming(true)}>
<Pencil className="mr-2 h-4 w-4" />Rename
</Button>
</DropdownMenuItem>
<DropdownMenuItem>
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => setIsSharing(true)}>
<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={() => setIsDeleting(true)}>
<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> </Link>
<ChatSessionActionMenu conversationId={props.conversation_id} />
</div> </div>
); );
} }
interface ChatSessionsModalProps { interface ChatSessionsModalProps {
data: ChatHistory[]; data: GroupedChatHistory | null;
setIsExpanded: React.Dispatch<React.SetStateAction<boolean>>;
} }
function ChatSessionsModal({data, setIsExpanded}: ChatSessionsModalProps) { function ChatSessionsModal({ data }: ChatSessionsModalProps) {
return ( return (
<div className={styles.modalSessionsList}> <Dialog>
<div className={styles.content}> <DialogTrigger
{data.map((chatHistory) => ( className="flex text-left text-medium text-gray-500 hover:text-gray-300 cursor-pointer my-4 text-sm p-[0.5rem]">
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} /> <span className="mr-2">See All <ArrowRight className="h-4 w-4" /></span>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>All Conversations</DialogTitle>
<DialogDescription
className="p-0">
<ScrollArea className="h-[500px] p-4">
{data && Object.keys(data).map((timeGrouping) => (
<div key={timeGrouping}>
<div className={`text-muted-foreground text-sm font-bold p-[0.5rem] `}>
{timeGrouping}
</div>
{data[timeGrouping].map((chatHistory) => (
<ChatSession
created={chatHistory.created}
compressed={false}
key={chatHistory.conversation_id}
conversation_id={chatHistory.conversation_id}
slug={chatHistory.slug}
agent_avatar={chatHistory.agent_avatar}
agent_name={chatHistory.agent_name} />
))} ))}
<button className={styles.showMoreButton} onClick={() => setIsExpanded(false)}>
Close
</button>
</div>
</div> </div>
))}
</ScrollArea>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
); );
} }
export default function SidePanel() { interface UserProfileProps {
userProfile: UserProfile;
webSocketConnected?: boolean;
collapsed: boolean;
}
function UserProfileComponent(props: UserProfileProps) {
if (props.collapsed) {
return (
<div className={styles.profile}>
<Avatar className="h-7 w-7">
<AvatarImage src={props.userProfile.photo} alt="user profile" />
<AvatarFallback>
{props.userProfile.username[0]}
</AvatarFallback>
</Avatar>
</div>
);
}
return (
<div className={styles.profile}>
<Link href="/config" target="_blank" rel="noopener noreferrer">
<Avatar>
<AvatarImage src={props.userProfile.photo} alt="user profile" />
<AvatarFallback>
{props.userProfile.username[0]}
</AvatarFallback>
</Avatar>
</Link>
<div className={styles.profileDetails}>
<p>{props.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>
);
}
const fetchChatHistory = async (url: string) => {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return response.json();
};
export const useChatSessionsFetchRequest = (url: string) => {
const { data, error } = useSWR<ChatHistory[]>(url, fetchChatHistory);
return {
data,
isLoading: !error && !data,
isError: error,
};
};
interface SidePanelProps {
webSocketConnected?: boolean;
conversationId: string | null;
uploadedFiles: string[];
}
export default function SidePanel(props: SidePanelProps) {
const [data, setData] = useState<ChatHistory[] | null>(null); const [data, setData] = useState<ChatHistory[] | null>(null);
const [dataToShow, setDataToShow] = useState<ChatHistory[] | null>(null); const [organizedData, setOrganizedData] = useState<GroupedChatHistory | null>(null);
const [isLoading, setLoading] = useState(true) const [subsetOrganizedData, setSubsetOrganizedData] = useState<GroupedChatHistory | null>(null);
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const [userProfile, setUserProfile] = useState<UserProfile | null>(null); const authenticatedData = useAuthenticatedData();
const { data: chatSessions } = useChatSessionsFetchRequest(authenticatedData ? `/api/chat/sessions` : '');
const [isMobileWidth, setIsMobileWidth] = useState(false);
useEffect(() => { useEffect(() => {
if (chatSessions) {
setData(chatSessions);
fetch('/api/chat/sessions', { method: 'GET' }) const groupedData: GroupedChatHistory = {};
.then(response => response.json()) const subsetOrganizedData: GroupedChatHistory = {};
.then((data: ChatHistory[]) => { let numAdded = 0;
setLoading(false);
// Render chat options, if any const currentDate = new Date();
if (data) {
setData(data); chatSessions.forEach((chatHistory) => {
setDataToShow(data.slice(0, 5)); const chatDate = new Date(chatHistory.created);
const diffTime = Math.abs(currentDate.getTime() - chatDate.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
const timeGrouping = diffDays < 7 ? 'Recent' : diffDays < 30 ? 'Last Month' : 'All Time';
if (!groupedData[timeGrouping]) {
groupedData[timeGrouping] = [];
}
groupedData[timeGrouping].push(chatHistory);
// Add to subsetOrganizedData if less than 8
if (numAdded < 8) {
if (!subsetOrganizedData[timeGrouping]) {
subsetOrganizedData[timeGrouping] = [];
}
subsetOrganizedData[timeGrouping].push(chatHistory);
numAdded++;
} }
})
.catch(err => {
console.error(err);
return;
}); });
fetch('/api/v1/user', { method: 'GET' })
.then(response => response.json()) setSubsetOrganizedData(subsetOrganizedData);
.then((data: UserProfile) => { setOrganizedData(groupedData);
setUserProfile(data); }
}) }, [chatSessions]);
.catch(err => {
console.error(err); useEffect(() => {
return; if (window.innerWidth < 768) {
setIsMobileWidth(true);
}
window.addEventListener('resize', () => {
setIsMobileWidth(window.innerWidth < 768);
}); });
}, []); }, []);
return ( return (
<div className={`${styles.panel}`}> <div className={`${styles.panel} ${enabled ? styles.expanded : styles.collapsed}`}>
{ <div className="flex items-start justify-between">
enabled ? <Image src="/khoj-logo.svg"
<div> alt="logo"
<div className={`${styles.expanded}`}> width={40}
<div className={`${styles.profile}`}> height={40}
{ userProfile && />
<div className={styles.profile}> {
<img authenticatedData &&
className={styles.profile} isMobileWidth ?
src={userProfile.photo} <Drawer>
alt="profile" <DrawerTrigger><ArrowRight className="h-4 w-4 mx-2" /></DrawerTrigger>
width={24} <DrawerContent>
height={24} <DrawerHeader>
<DrawerTitle>Sessions and Files</DrawerTitle>
<DrawerDescription>View all conversation sessions and manage conversation file filters</DrawerDescription>
</DrawerHeader>
<div className={`${styles.panelWrapper}`}>
<SessionsAndFiles
webSocketConnected={props.webSocketConnected}
setEnabled={setEnabled}
subsetOrganizedData={subsetOrganizedData}
organizedData={organizedData}
data={data}
uploadedFiles={props.uploadedFiles}
userProfile={authenticatedData}
conversationId={props.conversationId}
isMobileWidth={isMobileWidth}
/> />
<p>{userProfile?.username}</p>
</div> </div>
} <DrawerFooter>
</div> <DrawerClose>
<button className={styles.button} onClick={() => setEnabled(false)}> <Button variant="outline">Done</Button>
{/* Push Close Icon */} </DrawerClose>
<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> </DrawerFooter>
</DrawerContent>
</Drawer>
:
<button className={styles.button} onClick={() => setEnabled(!enabled)}>
{enabled ? <ArrowLeft className="h-4 w-4" /> : <ArrowRight className="h-4 w-4 mx-2" />}
</button> </button>
<h3>Recent Conversations</h3> }
</div>
<div className={styles.sessionsList}>
{dataToShow && dataToShow.map((chatHistory) => (
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} />
))}
</div> </div>
{ {
(data && data.length > 5) && ( authenticatedData && enabled &&
(isExpanded) ? <div className={`${styles.panelWrapper}`}>
<ChatSessionsModal data={data} setIsExpanded={setIsExpanded} /> <SessionsAndFiles
: webSocketConnected={props.webSocketConnected}
<button className={styles.showMoreButton} onClick={() => { setEnabled={setEnabled}
setIsExpanded(true); subsetOrganizedData={subsetOrganizedData}
}}> organizedData={organizedData}
Show All data={data}
</button> uploadedFiles={props.uploadedFiles}
) userProfile={authenticatedData}
} conversationId={props.conversationId}
</div> isMobileWidth={isMobileWidth}
:
<div>
<div className={`${styles.collapsed}`}>
{ userProfile &&
<div className={`${styles.profile}`}>
<img
className={styles.profile}
src={userProfile.photo}
alt="profile"
width={24}
height={24}
/> />
</div> </div>
} }
<button className={styles.button} onClick={() => setEnabled(true)}> {
{/* Pull Open Icon */} !authenticatedData && enabled &&
<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M15.2928932,12 L12.1464466,8.85355339 C11.9511845,8.65829124 11.9511845,8.34170876 12.1464466,8.14644661 C12.3417088,7.95118446 12.6582912,7.95118446 12.8535534,8.14644661 L16.8535534,12.1464466 C17.0488155,12.3417088 17.0488155,12.6582912 16.8535534,12.8535534 L12.8535534,16.8535534 C12.6582912,17.0488155 12.3417088,17.0488155 12.1464466,16.8535534 C11.9511845,16.6582912 11.9511845,16.3417088 12.1464466,16.1464466 L15.2928932,13 L4.5,13 C4.22385763,13 4,12.7761424 4,12.5 C4,12.2238576 4.22385763,12 4.5,12 L15.2928932,12 Z M19,5.5 C19,5.22385763 19.2238576,5 19.5,5 C19.7761424,5 20,5.22385763 20,5.5 L20,19.5 C20,19.7761424 19.7761424,20 19.5,20 C19.2238576,20 19,19.7761424 19,19.5 L19,5.5 Z"></path> </g></svg> <div className={`${styles.panelWrapper}`}>
</button> <Link href="/">
</div> <Button variant="ghost"><House className="h-4 w-4 mr-1" />Home</Button>
</Link>
<Link href="/">
<Button variant="ghost"><StackPlus className="h-4 w-4 mr-1" />New Conversation</Button>
</Link>
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`}> {/* Redirect to login page */}
<Button variant="default"><UserCirclePlus className="h-4 w-4 mr-1"/>Sign Up</Button>
</Link>
</div> </div>
} }
</div> </div>
); );
} }

View File

@@ -2,9 +2,28 @@ 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: hsla(var(--popover));
}
div.session:hover {
background-color: hsla(var(--popover));
color: hsla(var(--popover-foreground));
}
div.session a {
text-decoration: none;
} }
button.button { button.button {
@@ -17,29 +36,23 @@ 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;
} }
div.panel { div.panel {
display: grid; display: flex;
grid-auto-flow: row; flex-direction: column;
padding: 1rem; padding: 1rem;
border-radius: 1rem;
background-color: var(--calm-blue);
color: var(--main-text-color);
height: 100%;
overflow-y: auto; overflow-y: auto;
max-width: auto; max-width: auto;
transition: background-color 0.5s;
} }
div.expanded { div.expanded {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem; gap: 1rem;
background-color: hsla(var(--muted));
height: 100%;
} }
div.collapsed { div.collapsed {
@@ -47,14 +60,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 +73,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;
margin-top: auto;
}
div.panelWrapper {
display: grid;
grid-template-rows: auto 1fr auto auto;
height: 100%;
} }
@@ -75,7 +94,7 @@ div.modalSessionsList {
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: var(--frosted-background-color); background-color: hsla(var(--frosted-background-color));
z-index: 1; z-index: 1;
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -86,7 +105,7 @@ div.modalSessionsList {
div.modalSessionsList div.content { div.modalSessionsList div.content {
max-width: 80%; max-width: 80%;
max-height: 80%; max-height: 80%;
background-color: var(--frosted-background-color); background-color: hsla(var(--frosted-background-color));
overflow: auto; overflow: auto;
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
@@ -96,3 +115,33 @@ 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;
position: absolute;
width: 100%;
}
div.expanded {
z-index: 1;
}
div.singleReference {
padding: 4px;
}
div.panelWrapper {
width: 100%;
}
div.session.compressed {
max-width: 100%;
grid-template-columns: minmax(auto, 350px) 1fr;
}
div.session {
max-width: 100%;
grid-template-columns: 200px 1fr;
}
}

View File

@@ -140,6 +140,7 @@ function ReferenceVerification(props: ReferenceVerificationProps) {
const [initialResponse, setInitialResponse] = useState(""); const [initialResponse, setInitialResponse] = useState("");
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const verificationStatement = `${props.message}. Use this link for reference: ${props.additionalLink}`; const verificationStatement = `${props.message}. Use this link for reference: ${props.additionalLink}`;
const [isMobileWidth, setIsMobileWidth] = useState(false);
useEffect(() => { useEffect(() => {
if (props.prefilledResponse) { if (props.prefilledResponse) {
@@ -149,6 +150,12 @@ function ReferenceVerification(props: ReferenceVerificationProps) {
verifyStatement(verificationStatement, props.conversationId, setIsLoading, setInitialResponse, () => {}); verifyStatement(verificationStatement, props.conversationId, setIsLoading, setInitialResponse, () => {});
} }
setIsMobileWidth(window.innerWidth < 768);
window.addEventListener('resize', () => {
setIsMobileWidth(window.innerWidth < 768);
})
}, [verificationStatement, props.conversationId, props.prefilledResponse]); }, [verificationStatement, props.conversationId, props.prefilledResponse]);
useEffect(() => { useEffect(() => {
@@ -170,13 +177,12 @@ function ReferenceVerification(props: ReferenceVerificationProps) {
{ {
automationId: "", automationId: "",
by: "AI", by: "AI",
intent: {},
message: initialResponse, message: initialResponse,
context: [], context: [],
created: (new Date()).toISOString(), created: (new Date()).toISOString(),
onlineContext: {} onlineContext: {},
} }}
} setReferencePanelData={() => {}} setShowReferencePanel={() => {}} /> isMobileWidth={isMobileWidth} />
</div> </div>
) )
} }
@@ -236,6 +242,7 @@ export default function FactChecker() {
const [initialReferences, setInitialReferences] = useState<ResponseWithReferences>(); const [initialReferences, setInitialReferences] = useState<ResponseWithReferences>();
const [childReferences, setChildReferences] = useState<SupplementReferences[]>(); const [childReferences, setChildReferences] = useState<SupplementReferences[]>();
const [modelUsed, setModelUsed] = useState<Model>(); const [modelUsed, setModelUsed] = useState<Model>();
const [isMobileWidth, setIsMobileWidth] = useState(false);
const [conversationID, setConversationID] = useState(""); const [conversationID, setConversationID] = useState("");
const [runId, setRunId] = useState(""); const [runId, setRunId] = useState("");
@@ -251,6 +258,15 @@ export default function FactChecker() {
setChildReferences(newReferences); setChildReferences(newReferences);
} }
useEffect(() => {
setIsMobileWidth(window.innerWidth < 768);
window.addEventListener('resize', () => {
setIsMobileWidth(window.innerWidth < 768);
})
}, []);
let userData = useAuthenticatedData(); let userData = useAuthenticatedData();
function storeData() { function storeData() {
@@ -390,6 +406,7 @@ export default function FactChecker() {
const seenLinks = new Set(); const seenLinks = new Set();
// Any links that are present in webpages should not be searched again // Any links that are present in webpages should not be searched again
Object.entries(initialReferences.online || {}).map(([key, onlineData], index) => { Object.entries(initialReferences.online || {}).map(([key, onlineData], index) => {
const webpages = onlineData?.webpages || []; const webpages = onlineData?.webpages || [];
@@ -536,13 +553,12 @@ export default function FactChecker() {
{ {
automationId: "", automationId: "",
by: "AI", by: "AI",
intent: {},
message: initialResponse, message: initialResponse,
context: [], context: [],
created: (new Date()).toISOString(), created: (new Date()).toISOString(),
onlineContext: {} onlineContext: {}
} }
} setReferencePanelData={() => {}} setShowReferencePanel={() => {}} /> } isMobileWidth={isMobileWidth} />
</div> </div>
</CardContent> </CardContent>

View File

@@ -26,30 +26,313 @@
--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: 20 13% 95%;
--secondary-background-color: #F7F7F5;
--secondary-accent: #EDEDED;
--khoj-orange: #FFE7D1;
--border-color: #e2e2e2;
--box-shadow-color: rgba(0, 0, 0, 0.03);
/* Imported from Highlight.js */
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px
}
/*!
Theme: GitHub
Description: Light theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-light
Current colors taken from GitHub's CSS
*/
.hljs {
color: #24292e;
background: #ffffff
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
/* prettylights-syntax-keyword */
color: #d73a49
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
/* prettylights-syntax-entity */
color: #6f42c1
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-variable,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {
/* prettylights-syntax-constant */
color: #005cc5
}
.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {
/* prettylights-syntax-string */
color: #032f62
}
.hljs-built_in,
.hljs-symbol {
/* prettylights-syntax-variable */
color: #e36209
}
.hljs-comment,
.hljs-code,
.hljs-formula {
/* prettylights-syntax-comment */
color: #6a737d
}
.hljs-name,
.hljs-quote,
.hljs-selector-tag,
.hljs-selector-pseudo {
/* prettylights-syntax-entity-tag */
color: #22863a
}
.hljs-subst {
/* prettylights-syntax-storage-modifier-import */
color: #24292e
}
.hljs-section {
/* prettylights-syntax-markup-heading */
color: #005cc5;
font-weight: bold
}
.hljs-bullet {
/* prettylights-syntax-markup-list */
color: #735c0f
}
.hljs-emphasis {
/* prettylights-syntax-markup-italic */
color: #24292e;
font-style: italic
}
.hljs-strong {
/* prettylights-syntax-markup-bold */
color: #24292e;
font-weight: bold
}
.hljs-addition {
/* prettylights-syntax-markup-inserted */
color: #22863a;
background-color: #f0fff4
}
.hljs-deletion {
/* prettylights-syntax-markup-deleted */
color: #b31d28;
background-color: #ffeef0
}
.hljs-char.escape_,
.hljs-link,
.hljs-params,
.hljs-property,
.hljs-punctuation,
.hljs-tag {
/* purposely ignored */
}
} }
.dark { .dark {
--background: 224 71.4% 4.1%; --background: 0 0% 14%;
--foreground: 210 20% 98%; --foreground: 210 20% 98%;
--card: 224 71.4% 4.1%; --card: 0 0% 14%;
--card-foreground: 210 20% 98%; --card-foreground: 210 20% 98%;
--popover: 224 71.4% 4.1%; --popover: 0 0% 14%;
--popover-foreground: 210 20% 98%; --popover-foreground: 210 20% 98%;
--primary: 263.4 70% 50.4%; --primary: 263.4 70% 50.4%;
--primary-foreground: 210 20% 98%; --primary-foreground: 210 20% 98%;
--secondary: 215 27.9% 16.9%; --secondary: 0 0% 9%;
--secondary-foreground: 210 20% 98%; --secondary-foreground: 210 20% 98%;
--muted: 215 27.9% 16.9%; --muted: 0 0% 9%;
--muted-foreground: 217.9 10.6% 64.9%; --muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%; --accent: 0 0% 9%;
--accent-foreground: 210 20% 98%; --accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%; --destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%; --border: 0 0% 9%;
--input: 215 27.9% 16.9%; --input: 0 0% 9%;
--ring: 263.4 70% 50.4%; --ring: 263.4 70% 50.4%;
--font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important; --font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important;
--box-shadow-color: rgba(255, 255, 255, 0.05);
/* Imported from highlight.js */
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px
}
/*!
Theme: GitHub Dark
Description: Dark theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-dark
Current colors taken from GitHub's CSS
*/
.hljs {
color: #c9d1d9;
background: #0d1117
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
/* prettylights-syntax-keyword */
color: #ff7b72
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
/* prettylights-syntax-entity */
color: #d2a8ff
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-variable,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {
/* prettylights-syntax-constant */
color: #79c0ff
}
.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {
/* prettylights-syntax-string */
color: #a5d6ff
}
.hljs-built_in,
.hljs-symbol {
/* prettylights-syntax-variable */
color: #ffa657
}
.hljs-comment,
.hljs-code,
.hljs-formula {
/* prettylights-syntax-comment */
color: #8b949e
}
.hljs-name,
.hljs-quote,
.hljs-selector-tag,
.hljs-selector-pseudo {
/* prettylights-syntax-entity-tag */
color: #7ee787
}
.hljs-subst {
/* prettylights-syntax-storage-modifier-import */
color: #c9d1d9
}
.hljs-section {
/* prettylights-syntax-markup-heading */
color: #1f6feb;
font-weight: bold
}
.hljs-bullet {
/* prettylights-syntax-markup-list */
color: #f2cc60
}
.hljs-emphasis {
/* prettylights-syntax-markup-italic */
color: #c9d1d9;
font-style: italic
}
.hljs-strong {
/* prettylights-syntax-markup-bold */
color: #c9d1d9;
font-weight: bold
}
.hljs-addition {
/* prettylights-syntax-markup-inserted */
color: #aff5b4;
background-color: #033a16
}
.hljs-deletion {
/* prettylights-syntax-markup-deleted */
color: #ffdcd7;
background-color: #67060c
}
.hljs-char.escape_,
.hljs-link,
.hljs-params,
.hljs-property,
.hljs-punctuation,
.hljs-tag {
/* purposely ignored */
}
} }
} }

View File

@@ -108,6 +108,7 @@
.logo { .logo {
position: relative; position: relative;
} }
/* Enable hover only on non-touch devices */ /* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) { @media (hover: hover) and (pointer: fine) {
.card:hover { .card:hover {
@@ -179,11 +180,9 @@
border-radius: 0; border-radius: 0;
border: none; border: none;
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
background: linear-gradient( background: linear-gradient(to bottom,
to bottom,
rgba(var(--background-start-rgb), 1), rgba(var(--background-start-rgb), 1),
rgba(var(--callout-rgb), 0.5) rgba(var(--callout-rgb), 0.5));
);
background-clip: padding-box; background-clip: padding-box;
backdrop-filter: blur(24px); backdrop-filter: blur(24px);
} }
@@ -194,11 +193,9 @@
inset: auto 0 0; inset: auto 0 0;
padding: 2rem; padding: 2rem;
height: 200px; height: 200px;
background: linear-gradient( background: linear-gradient(to bottom,
to bottom,
transparent 0%, transparent 0%,
rgb(var(--background-end-rgb)) 40% rgb(var(--background-end-rgb)) 40%);
);
z-index: 1; z-index: 1;
} }
} }
@@ -211,10 +208,6 @@
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.vercelLogo {
filter: invert(1);
}
.logo { .logo {
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
} }
@@ -224,6 +217,7 @@
from { from {
transform: rotate(360deg); transform: rotate(360deg);
} }
to { to {
transform: rotate(0deg); transform: rotate(0deg);
} }

View File

@@ -0,0 +1,33 @@
import type { Metadata } from "next";
import { Noto_Sans } from "next/font/google";
import "../../globals.css";
const inter = Noto_Sans({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Khoj AI - Chat",
description: "Use this page to view a chat with Khoj AI.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<meta httpEquiv="Content-Security-Policy"
content="default-src 'self' https://assets.khoj.dev;
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
child-src 'none';
object-src 'none';"></meta>
<body className={inter.className}>
{children}
</body>
</html>
);
}

View File

@@ -0,0 +1,324 @@
'use client'
import styles from './sharedChat.module.css';
import React, { Suspense, useEffect, useRef, useState } from 'react';
import SidePanel from '../../components/sidePanel/chatHistorySidePanel';
import ChatHistory from '../../components/chatHistory/chatHistory';
import NavMenu from '../../components/navMenu/navMenu';
import Loading from '../../components/loading/loading';
import 'katex/dist/katex.min.css';
import { welcomeConsole } from '../../common/utils';
import { useAuthenticatedData } from '@/app/common/auth';
import ChatInputArea, { ChatOptions } from '@/app/components/chatInputArea/chatInputArea';
import { StreamMessage } from '@/app/components/chatMessage/chatMessage';
import { handleCompiledReferences, handleImageResponse, setupWebSocket } from '@/app/common/chatFunctions';
interface ChatBodyDataProps {
chatOptionsData: ChatOptions | null;
setTitle: (title: string) => void;
setUploadedFiles: (files: string[]) => void;
isMobileWidth?: boolean;
publicConversationSlug: string;
streamedMessages: StreamMessage[];
isLoggedIn: boolean;
conversationId?: string;
setQueryToProcess: (query: string) => void;
}
function ChatBodyData(props: ChatBodyDataProps) {
const [message, setMessage] = useState('');
const [processingMessage, setProcessingMessage] = useState(false);
useEffect(() => {
if (message) {
setProcessingMessage(true);
props.setQueryToProcess(message);
}
}, [message]);
useEffect(() => {
console.log("Streamed messages", props.streamedMessages);
if (props.streamedMessages &&
props.streamedMessages.length > 0 &&
props.streamedMessages[props.streamedMessages.length - 1].completed) {
setProcessingMessage(false);
} else {
setMessage('');
}
}, [props.streamedMessages]);
if (!props.publicConversationSlug && !props.conversationId) {
return (
<div className={styles.suggestions}>
Whoops, nothing to see here!
</div>
);
}
return (
<>
<div className={false ? styles.chatBody : styles.chatBodyFull}>
<ChatHistory
publicConversationSlug={props.publicConversationSlug}
conversationId={props.conversationId || ''}
setTitle={props.setTitle}
pendingMessage={processingMessage ? message : ''}
incomingMessages={props.streamedMessages} />
</div>
<div className={`${styles.inputBox} bg-background align-middle items-center justify-center px-3`}>
<ChatInputArea
isLoggedIn={props.isLoggedIn}
sendMessage={(message) => setMessage(message)}
sendDisabled={processingMessage}
chatOptionsData={props.chatOptionsData}
conversationId={props.conversationId}
isMobileWidth={props.isMobileWidth}
setUploadedFiles={props.setUploadedFiles} />
</div>
</>
);
}
export default function SharedChat() {
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
const [isLoading, setLoading] = useState(true);
const [title, setTitle] = useState('Khoj AI - Chat');
const [conversationId, setConversationID] = useState<string | undefined>(undefined);
const [chatWS, setChatWS] = useState<WebSocket | null>(null);
const [messages, setMessages] = useState<StreamMessage[]>([]);
const [queryToProcess, setQueryToProcess] = useState<string>('');
const [processQuerySignal, setProcessQuerySignal] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
const [isMobileWidth, setIsMobileWidth] = useState(false);
const [paramSlug, setParamSlug] = useState<string | undefined>(undefined);
const authenticatedData = useAuthenticatedData();
welcomeConsole();
const handleWebSocketMessage = (event: MessageEvent) => {
let chunk = event.data;
let currentMessage = messages.find(message => !message.completed);
if (!currentMessage) {
console.error("No current message found");
return;
}
// Process WebSocket streamed data
if (chunk === "start_llm_response") {
console.log("Started streaming", new Date());
} else if (chunk === "end_llm_response") {
currentMessage.completed = true;
} else {
// Get the current message
// Process and update state with the new message
if (chunk.includes("application/json")) {
chunk = JSON.parse(chunk);
}
const contentType = chunk["content-type"];
if (contentType === "application/json") {
try {
if (chunk.image || chunk.detail) {
let responseWithReference = handleImageResponse(chunk);
console.log("Image response", responseWithReference);
if (responseWithReference.response) currentMessage.rawResponse = responseWithReference.response;
if (responseWithReference.online) currentMessage.onlineContext = responseWithReference.online;
if (responseWithReference.context) currentMessage.context = responseWithReference.context;
} else if (chunk.type == "status") {
currentMessage.trainOfThought.push(chunk.message);
} else if (chunk.type == "rate_limit") {
console.log("Rate limit message", chunk);
currentMessage.rawResponse = chunk.message;
} else {
console.log("any message", chunk);
}
} catch (error) {
console.error("Error processing message", error);
currentMessage.completed = true;
} finally {
// no-op
}
} else {
// Update the current message with the new chunk
if (chunk && chunk.includes("### compiled references:")) {
let responseWithReference = handleCompiledReferences(chunk, "");
currentMessage.rawResponse += responseWithReference.response;
if (responseWithReference.response) currentMessage.rawResponse = responseWithReference.response;
if (responseWithReference.online) currentMessage.onlineContext = responseWithReference.online;
if (responseWithReference.context) currentMessage.context = responseWithReference.context;
} else {
// If the chunk is not a JSON object, just display it as is
currentMessage.rawResponse += chunk;
}
}
};
// Update the state with the new message, currentMessage
setMessages([...messages]);
}
useEffect(() => {
fetch('/api/chat/options')
.then(response => response.json())
.then((data: ChatOptions) => {
setLoading(false);
// Render chat options, if any
if (data) {
setChatOptionsData(data);
}
})
.catch(err => {
console.error(err);
return;
});
setIsMobileWidth(window.innerWidth < 786);
window.addEventListener('resize', () => {
setIsMobileWidth(window.innerWidth < 786);
});
setParamSlug(window.location.pathname.split('/').pop() || '');
}, []);
useEffect(() => {
if (queryToProcess && !conversationId) {
fetch(`/api/chat/share/fork?public_conversation_slug=${paramSlug}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
setConversationID(data.conversation_id);
})
.catch(err => {
console.error(err);
return;
});
return;
}
if (chatWS && queryToProcess) {
// Add a new object to the state
const newStreamMessage: StreamMessage = {
rawResponse: "",
trainOfThought: [],
context: [],
onlineContext: {},
completed: false,
timestamp: (new Date()).toISOString(),
rawQuery: queryToProcess || "",
}
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
setProcessQuerySignal(true);
} else {
if (!chatWS) {
console.error("No WebSocket connection available");
}
if (!queryToProcess) {
console.error("No query to process");
}
}
}, [queryToProcess]);
useEffect(() => {
if (processQuerySignal && chatWS) {
setProcessQuerySignal(false);
chatWS.onmessage = handleWebSocketMessage;
chatWS?.send(queryToProcess);
}
}, [processQuerySignal]);
useEffect(() => {
if (chatWS) {
chatWS.onmessage = handleWebSocketMessage;
}
}, [chatWS]);
useEffect(() => {
(async () => {
if (conversationId) {
const newWS = await setupWebSocket(conversationId, queryToProcess);
if (!newWS) {
console.error("No WebSocket connection available");
return;
}
setChatWS(newWS);
// Add a new object to the state
const newStreamMessage: StreamMessage = {
rawResponse: "",
trainOfThought: [],
context: [],
onlineContext: {},
completed: false,
timestamp: (new Date()).toISOString(),
rawQuery: queryToProcess || "",
}
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
}
})();
}, [conversationId]);
if (isLoading) {
return <Loading />;
}
if (!paramSlug) {
return (
<div className={styles.suggestions}>
Whoops, nothing to see here!
</div>
);
}
return (
<div className={`${styles.main} ${styles.chatLayout}`}>
<title>
{title}
</title>
<div className={styles.sidePanel}>
<SidePanel
webSocketConnected={!!conversationId ? (chatWS != null) : true}
conversationId={conversationId ?? null}
uploadedFiles={uploadedFiles} />
</div>
<div className={styles.chatBox}>
<NavMenu selected="Chat" title={title} />
<div className={styles.chatBoxBody}>
<Suspense fallback={<Loading />}>
<ChatBodyData
conversationId={conversationId}
streamedMessages={messages}
setQueryToProcess={setQueryToProcess}
isLoggedIn={authenticatedData !== null}
publicConversationSlug={paramSlug}
chatOptionsData={chatOptionsData}
setTitle={setTitle}
setUploadedFiles={setUploadedFiles}
isMobileWidth={isMobileWidth} />
</Suspense>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,127 @@
div.main {
height: 100vh;
color: hsla(var(--foreground));
}
.suggestions {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
justify-content: center;
}
div.inputBox {
border: 1px solid var(--border-color);
border-radius: 16px;
box-shadow: 0 4px 10px var(--box-shadow-color);
margin-bottom: 20px;
gap: 12px;
padding-left: 20px;
padding-right: 20px;
align-content: center;
}
input.inputBox {
border: none;
}
input.inputBox:focus {
outline: none;
background-color: transparent;
}
div.inputBox:focus {
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
}
div.chatBodyFull {
display: grid;
grid-template-columns: 1fr;
height: 100%;
}
button.inputBox {
border: none;
outline: none;
background-color: transparent;
cursor: pointer;
border-radius: 0.5rem;
padding: 0.5rem;
background: linear-gradient(var(--calm-green), var(--calm-blue));
}
div.chatBody {
display: grid;
grid-template-columns: 1fr 1fr;
height: 100%;
}
.inputBox {
color: hsla(var(--foreground));
}
div.chatLayout {
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
}
div.chatBox {
display: grid;
height: 100%;
}
div.titleBar {
display: grid;
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;
}
div.chatBox {
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;
grid-template-columns: 1fr;
}
}

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,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@@ -0,0 +1,155 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className={cn("mr-2 h-4 w-4 shrink-0 opacity-50", className)} />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@@ -0,0 +1,118 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

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,31 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
interface ProgressProps extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
indicatorColor?: string
}
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
ProgressProps
>(({ className, value, indicatorColor, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className={`h-full w-full flex-1 bg-primary transition-all ${indicatorColor}`}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

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,140 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

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

@@ -0,0 +1,45 @@
"use client"
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }

View File

@@ -17,15 +17,28 @@
"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-collapsible": "^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-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toggle": "^1.1.0",
"@types/dompurify": "^3.0.5",
"@types/katex": "^0.16.7", "@types/katex": "^0.16.7",
"@types/markdown-it": "^14.1.1", "@types/markdown-it": "^14.1.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dompurify": "^3.1.6",
"katex": "^0.16.10", "katex": "^0.16.10",
"lucide-react": "^0.397.0", "lucide-react": "^0.397.0",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
@@ -38,7 +51,8 @@
"swr": "^2.2.5", "swr": "^2.2.5",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.4",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",

View File

@@ -7,6 +7,7 @@
], ],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"downlevelIteration": true,
"strict": true, "strict": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,

View File

@@ -245,7 +245,7 @@
"@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7"
"@babel/plugin-syntax-typescript" "^7.24.7" "@babel/plugin-syntax-typescript" "^7.24.7"
"@babel/runtime@^7.23.2", "@babel/runtime@^7.24.1": "@babel/runtime@^7.13.10", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.1":
version "7.24.7" version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
@@ -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,28 @@
"@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.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd"
integrity sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==
dependencies:
"@babel/runtime" "^7.13.10"
"@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,17 +525,93 @@
"@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-collapsible@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz#4d49ddcc7b7d38f6c82f1fd29674f6fab5353e77"
integrity sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==
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-presence" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-controllable-state" "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.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989"
integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==
dependencies:
"@babel/runtime" "^7.13.10"
"@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"
integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw== integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==
"@radix-ui/react-context@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c"
integrity sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-context@1.1.0": "@radix-ui/react-context@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8"
integrity sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A== integrity sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==
"@radix-ui/react-dialog@1.1.1", "@radix-ui/react-dialog@^1.1.1": "@radix-ui/react-dialog@1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300"
integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-dismissable-layer" "1.0.5"
"@radix-ui/react-focus-guards" "1.0.1"
"@radix-ui/react-focus-scope" "1.0.4"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-portal" "1.0.4"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-use-controllable-state" "1.0.1"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-dialog@1.1.1", "@radix-ui/react-dialog@^1.0.4", "@radix-ui/react-dialog@^1.1.1":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz#4906507f7b4ad31e22d7dad69d9330c87c431d44" resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz#4906507f7b4ad31e22d7dad69d9330c87c431d44"
integrity sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg== integrity sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==
@@ -511,6 +631,23 @@
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.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4"
integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown" "1.0.3"
"@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,11 +659,41 @@
"@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.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad"
integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==
dependencies:
"@babel/runtime" "^7.13.10"
"@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"
integrity sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw== integrity sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==
"@radix-ui/react-focus-scope@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525"
integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-focus-scope@1.1.0": "@radix-ui/react-focus-scope@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2" resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2"
@@ -536,6 +703,14 @@
"@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-id@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0"
integrity sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-id@1.1.0": "@radix-ui/react-id@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed"
@@ -550,6 +725,111 @@
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-popover@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.1.tgz#604b783cdb3494ed4f16a58c17f0e81e61ab7775"
integrity sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==
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-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-slot" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.7"
"@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.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15"
integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@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"
@@ -558,6 +838,15 @@
"@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-layout-effect" "1.1.0" "@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-presence@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"
integrity sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-presence@1.1.0": "@radix-ui/react-presence@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.0.tgz#227d84d20ca6bfe7da97104b1a8b48a833bfb478" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.0.tgz#227d84d20ca6bfe7da97104b1a8b48a833bfb478"
@@ -566,6 +855,14 @@
"@radix-ui/react-compose-refs" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0" "@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-primitive@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0"
integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-primitive@2.0.0": "@radix-ui/react-primitive@2.0.0":
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884" resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884"
@@ -573,6 +870,52 @@
dependencies: dependencies:
"@radix-ui/react-slot" "1.1.0" "@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-progress@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.0.tgz#28c267885ec154fc557ec7a66cb462787312f7e2"
integrity sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==
dependencies:
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-primitive" "2.0.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.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab"
integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
"@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"
@@ -580,11 +923,35 @@
dependencies: dependencies:
"@radix-ui/react-compose-refs" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-toggle@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz#1f7697b82917019330a16c6f96f649f46b4606cf"
integrity sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-use-callback-ref@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"
integrity sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref@1.1.0": "@radix-ui/react-use-callback-ref@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1"
integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw== integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==
"@radix-ui/react-use-controllable-state@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286"
integrity sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-controllable-state@1.1.0": "@radix-ui/react-use-controllable-state@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0"
@@ -592,6 +959,14 @@
dependencies: dependencies:
"@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.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755"
integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown@1.1.0": "@radix-ui/react-use-escape-keydown@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754"
@@ -599,11 +974,49 @@
dependencies: dependencies:
"@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399"
integrity sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect@1.1.0": "@radix-ui/react-use-layout-effect@1.1.0":
version "1.1.0" version "1.1.0"
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"
@@ -632,6 +1045,13 @@
mkdirp "^2.1.6" mkdirp "^2.1.6"
path-browserify "^1.0.1" path-browserify "^1.0.1"
"@types/dompurify@^3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7"
integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==
dependencies:
"@types/trusted-types" "*"
"@types/json5@^0.0.29": "@types/json5@^0.0.29":
version "0.0.29" version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@@ -687,6 +1107,11 @@
"@types/prop-types" "*" "@types/prop-types" "*"
csstype "^3.0.2" csstype "^3.0.2"
"@types/trusted-types@*":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0": "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0":
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.2.0.tgz#44356312aea8852a3a82deebdacd52ba614ec07a" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.2.0.tgz#44356312aea8852a3a82deebdacd52ba614ec07a"
@@ -1161,6 +1586,14 @@ clsx@^2.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
cmdk@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-1.0.0.tgz#0a095fdafca3dfabed82d1db78a6262fb163ded9"
integrity sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==
dependencies:
"@radix-ui/react-dialog" "1.0.5"
"@radix-ui/react-primitive" "1.0.3"
code-block-writer@^12.0.0: code-block-writer@^12.0.0:
version "12.0.0" version "12.0.0"
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-12.0.0.tgz#4dd58946eb4234105aff7f0035977b2afdc2a770" resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-12.0.0.tgz#4dd58946eb4234105aff7f0035977b2afdc2a770"
@@ -1371,6 +1804,11 @@ doctrine@^3.0.0:
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
dompurify@^3.1.6:
version "3.1.6"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.6.tgz#43c714a94c6a7b8801850f82e756685300a027e2"
integrity sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==
eastasianwidth@^0.2.0: eastasianwidth@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
@@ -3164,7 +3602,7 @@ react-is@^16.13.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-remove-scroll-bar@^2.3.4: react-remove-scroll-bar@^2.3.3, react-remove-scroll-bar@^2.3.4:
version "2.3.6" version "2.3.6"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c"
integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==
@@ -3172,6 +3610,17 @@ react-remove-scroll-bar@^2.3.4:
react-style-singleton "^2.2.1" react-style-singleton "^2.2.1"
tslib "^2.0.0" tslib "^2.0.0"
react-remove-scroll@2.5.5:
version "2.5.5"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77"
integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==
dependencies:
react-remove-scroll-bar "^2.3.3"
react-style-singleton "^2.2.1"
tslib "^2.1.0"
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-remove-scroll@2.5.7: react-remove-scroll@2.5.7:
version "2.5.7" version "2.5.7"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz#15a1fd038e8497f65a695bf26a4a57970cac1ccb" resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz#15a1fd038e8497f65a695bf26a4a57970cac1ccb"
@@ -3884,6 +4333,13 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
vaul@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/vaul/-/vaul-0.9.1.tgz#3640198e04636b209b1f907fcf3079bec6ecc66b"
integrity sha512-fAhd7i4RNMinx+WEm6pF3nOl78DFkAazcN04ElLPFF9BMCNGbY/kou8UMhIcicm0rJCNePJP0Yyza60gGOD0Jw==
dependencies:
"@radix-ui/react-dialog" "^1.0.4"
wcwidth@^1.0.1: wcwidth@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"

View File

@@ -645,7 +645,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(
@@ -799,11 +803,14 @@ class ConversationAdapters:
def create_conversation_from_public_conversation( def create_conversation_from_public_conversation(
user: KhojUser, public_conversation: PublicConversation, client_app: ClientApplication user: KhojUser, public_conversation: PublicConversation, client_app: ClientApplication
): ):
scrubbed_title = public_conversation.title if public_conversation.title else public_conversation.slug
if scrubbed_title:
scrubbed_title = scrubbed_title.replace("-", " ")
return Conversation.objects.create( return Conversation.objects.create(
user=user, user=user,
conversation_log=public_conversation.conversation_log, conversation_log=public_conversation.conversation_log,
client=client_app, client=client_app,
slug=public_conversation.slug, slug=scrubbed_title,
title=public_conversation.title, title=public_conversation.title,
agent=public_conversation.agent, agent=public_conversation.agent,
) )
@@ -944,6 +951,34 @@ class ConversationAdapters:
) )
return new_config return new_config
@staticmethod
def add_files_to_filter(user: KhojUser, conversation_id: int, files: List[str]):
conversation = ConversationAdapters.get_conversation_by_user(user, conversation_id=conversation_id)
file_list = EntryAdapters.get_all_filenames_by_source(user, "computer")
for filename in files:
if filename in file_list and filename not in conversation.file_filters:
conversation.file_filters.append(filename)
conversation.save()
# remove files from conversation.file_filters that are not in file_list
conversation.file_filters = [file for file in conversation.file_filters if file in file_list]
conversation.save()
return conversation.file_filters
@staticmethod
def remove_files_from_filter(user: KhojUser, conversation_id: int, files: List[str]):
conversation = ConversationAdapters.get_conversation_by_user(user, conversation_id=conversation_id)
for filename in files:
if filename in conversation.file_filters:
conversation.file_filters.remove(filename)
conversation.save()
# remove files from conversation.file_filters that are not in file_list
file_list = EntryAdapters.get_all_filenames_by_source(user, "computer")
conversation.file_filters = [file for file in conversation.file_filters if file in file_list]
conversation.save()
return conversation.file_filters
class FileObjectAdapters: class FileObjectAdapters:
@staticmethod @staticmethod

View File

@@ -372,7 +372,7 @@ async def extract_references_and_questions(
logger.info(f"🔍 Searching knowledge base with queries: {inferred_queries}") logger.info(f"🔍 Searching knowledge base with queries: {inferred_queries}")
if send_status_func: if send_status_func:
inferred_queries_str = "\n- " + "\n- ".join(inferred_queries) inferred_queries_str = "\n- " + "\n- ".join(inferred_queries)
await send_status_func(f"**🔍 Searching Documents for:** {inferred_queries_str}") await send_status_func(f"**Searching Documents for:** {inferred_queries_str}")
for query in inferred_queries: for query in inferred_queries:
n_items = min(n, 3) if using_offline_chat else n n_items = min(n, 3) if using_offline_chat else n
search_results.extend( search_results.extend(

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
@@ -16,7 +16,6 @@ from websockets import ConnectionClosedOK
from khoj.app.settings import ALLOWED_HOSTS from khoj.app.settings import ALLOWED_HOSTS
from khoj.database.adapters import ( from khoj.database.adapters import (
ConversationAdapters, ConversationAdapters,
DataStoreAdapters,
EntryAdapters, EntryAdapters,
FileObjectAdapters, FileObjectAdapters,
PublicConversationAdapters, PublicConversationAdapters,
@@ -58,7 +57,7 @@ from khoj.utils.helpers import (
get_device, get_device,
is_none_or_empty, is_none_or_empty,
) )
from khoj.utils.rawconfig import FilterRequest, LocationData from khoj.utils.rawconfig import FileFilterRequest, FilesFilterRequest, LocationData
# Initialize Router # Initialize Router
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -92,68 +91,36 @@ 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): @api_chat.delete("/conversation/file-filters/bulk", response_class=Response)
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"]) @requires(["authenticated"])
async def store_factchecker(request: Request, common: CommonQueryParams, data: FactCheckerStoreData): def remove_files_filter(request: Request, filter: FilesFilterRequest) -> Response:
user = request.user.object conversation_id = int(filter.conversation_id)
files_filter = filter.filenames
update_telemetry_state( file_filters = ConversationAdapters.remove_files_from_filter(request.user.object, conversation_id, files_filter)
request=request, return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
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) @api_chat.post("/conversation/file-filters/bulk", response_class=Response)
async def get_factchecker(request: Request, common: CommonQueryParams, runId: str): @requires(["authenticated"])
update_telemetry_state( def add_files_filter(request: Request, filter: FilesFilterRequest):
request=request, try:
telemetry_type="api", conversation_id = int(filter.conversation_id)
api="read_factchecker", files_filter = filter.filenames
**common.__dict__, file_filters = ConversationAdapters.add_files_to_filter(request.user.object, conversation_id, files_filter)
) return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
except Exception as e:
fact_checker_key = f"factchecker_{runId}" logger.error(f"Error adding file filter {filter.filename}: {e}", exc_info=True)
raise HTTPException(status_code=422, detail=str(e))
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: FileFilterRequest):
try: try:
conversation = ConversationAdapters.get_conversation_by_user( conversation_id = int(filter.conversation_id)
request.user.object, conversation_id=int(filter.conversation_id) files_filter = [filter.filename]
) file_filters = ConversationAdapters.add_files_to_filter(request.user.object, conversation_id, files_filter)
file_list = EntryAdapters.get_all_filenames_by_source(request.user.object, "computer") return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
if filter.filename in file_list and filter.filename not in conversation.file_filters:
conversation.file_filters.append(filter.filename)
conversation.save()
# remove files from conversation.file_filters that are not in file_list
conversation.file_filters = [file for file in conversation.file_filters if file in file_list]
conversation.save()
return Response(content=json.dumps(conversation.file_filters), media_type="application/json", status_code=200)
except Exception as e: except Exception as e:
logger.error(f"Error adding file filter {filter.filename}: {e}", exc_info=True) logger.error(f"Error adding file filter {filter.filename}: {e}", exc_info=True)
raise HTTPException(status_code=422, detail=str(e)) raise HTTPException(status_code=422, detail=str(e))
@@ -161,18 +128,11 @@ def add_file_filter(request: Request, filter: FilterRequest):
@api_chat.delete("/conversation/file-filters", response_class=Response) @api_chat.delete("/conversation/file-filters", response_class=Response)
@requires(["authenticated"]) @requires(["authenticated"])
def remove_file_filter(request: Request, filter: FilterRequest) -> Response: def remove_file_filter(request: Request, filter: FileFilterRequest) -> Response:
conversation = ConversationAdapters.get_conversation_by_user( conversation_id = int(filter.conversation_id)
request.user.object, conversation_id=int(filter.conversation_id) files_filter = [filter.filename]
) file_filters = ConversationAdapters.remove_files_from_filter(request.user.object, conversation_id, files_filter)
if filter.filename in conversation.file_filters: return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
conversation.file_filters.remove(filter.filename)
conversation.save()
# remove files from conversation.file_filters that are not in file_list
file_list = EntryAdapters.get_all_filenames_by_source(request.user.object, "computer")
conversation.file_filters = [file for file in conversation.file_filters if file in file_list]
conversation.save()
return Response(content=json.dumps(conversation.file_filters), media_type="application/json", status_code=200)
class FeedbackData(BaseModel): class FeedbackData(BaseModel):
@@ -309,10 +269,15 @@ def get_shared_chat(
} }
meta_log = conversation.conversation_log meta_log = conversation.conversation_log
scrubbed_title = conversation.title if conversation.title else conversation.slug
if scrubbed_title:
scrubbed_title = scrubbed_title.replace("-", " ")
meta_log.update( meta_log.update(
{ {
"conversation_id": conversation.id, "conversation_id": conversation.id,
"slug": conversation.title if conversation.title else conversation.slug, "slug": scrubbed_title,
"agent": agent_metadata, "agent": agent_metadata,
} }
) )
@@ -328,7 +293,7 @@ def get_shared_chat(
update_telemetry_state( update_telemetry_state(
request=request, request=request,
telemetry_type="api", telemetry_type="api",
api="public_conversation_history", api="chat_history",
**common.__dict__, **common.__dict__,
) )
@@ -370,7 +335,7 @@ def fork_public_conversation(
public_conversation = PublicConversationAdapters.get_public_conversation_by_slug(public_conversation_slug) public_conversation = PublicConversationAdapters.get_public_conversation_by_slug(public_conversation_slug)
# Duplicate Public Conversation to User's Private Conversation # Duplicate Public Conversation to User's Private Conversation
ConversationAdapters.create_conversation_from_public_conversation( new_conversation = ConversationAdapters.create_conversation_from_public_conversation(
user, public_conversation, request.user.client_app user, public_conversation, request.user.client_app
) )
@@ -386,7 +351,16 @@ def fork_public_conversation(
redirect_uri = str(request.app.url_path_for("chat_page")) redirect_uri = str(request.app.url_path_for("chat_page"))
return Response(status_code=200, content=json.dumps({"status": "ok", "next_url": redirect_uri})) return Response(
status_code=200,
content=json.dumps(
{
"status": "ok",
"next_url": redirect_uri,
"conversation_id": new_conversation.id,
}
),
)
@api_chat.post("/share") @api_chat.post("/share")
@@ -427,15 +401,29 @@ 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]
sessions = conversations.values_list(
"id", "slug", "title", "agent__slug", "agent__name", "agent__avatar", "created_at"
) )
session_values = [{"conversation_id": session[0], "slug": session[2] or session[1]} for session in sessions] session_values = [
{
"conversation_id": session[0],
"slug": session[2] or session[1],
"agent_name": session[4],
"agent_avatar": session[5],
"created": session[6].strftime("%Y-%m-%d %H:%M:%S"),
}
for session in sessions
]
update_telemetry_state( update_telemetry_state(
request=request, request=request,
@@ -477,7 +465,6 @@ async def create_chat_session(
@api_chat.get("/options", response_class=Response) @api_chat.get("/options", response_class=Response)
@requires(["authenticated"])
async def chat_options( async def chat_options(
request: Request, request: Request,
common: CommonQueryParams, common: CommonQueryParams,
@@ -641,7 +628,7 @@ async def websocket_endpoint(
user_message_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") user_message_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
conversation_commands = [get_conversation_command(query=q, any_references=True)] conversation_commands = [get_conversation_command(query=q, any_references=True)]
await send_status_update(f"**👀 Understanding Query**: {q}") await send_status_update(f"**Understanding Query**: {q}")
meta_log = conversation.conversation_log meta_log = conversation.conversation_log
is_automated_task = conversation_commands == [ConversationCommand.AutomatedTask] is_automated_task = conversation_commands == [ConversationCommand.AutomatedTask]
@@ -650,10 +637,10 @@ async def websocket_endpoint(
if conversation_commands == [ConversationCommand.Default] or is_automated_task: if conversation_commands == [ConversationCommand.Default] or is_automated_task:
conversation_commands = await aget_relevant_information_sources(q, meta_log, is_automated_task) conversation_commands = await aget_relevant_information_sources(q, meta_log, is_automated_task)
conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands]) conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands])
await send_status_update(f"**🗃️ Chose Data Sources to Search:** {conversation_commands_str}") await send_status_update(f"**Chose Data Sources to Search:** {conversation_commands_str}")
mode = await aget_relevant_output_modes(q, meta_log, is_automated_task) mode = await aget_relevant_output_modes(q, meta_log, is_automated_task)
await send_status_update(f"**🧑🏾‍💻 Decided Response Mode:** {mode.value}") await send_status_update(f"**Decided Response Mode:** {mode.value}")
if mode not in conversation_commands: if mode not in conversation_commands:
conversation_commands.append(mode) conversation_commands.append(mode)
@@ -690,7 +677,7 @@ async def websocket_endpoint(
contextual_data = " ".join([file.raw_text for file in file_object]) contextual_data = " ".join([file.raw_text for file in file_object])
if not q: if not q:
q = "Create a general summary of the file" q = "Create a general summary of the file"
await send_status_update(f"**🧑🏾‍💻 Constructing Summary Using:** {file_object[0].file_name}") await send_status_update(f"**Constructing Summary Using:** {file_object[0].file_name}")
response = await extract_relevant_summary(q, contextual_data) response = await extract_relevant_summary(q, contextual_data)
response_log = str(response) response_log = str(response)
await send_complete_llm_response(response_log) await send_complete_llm_response(response_log)
@@ -775,7 +762,7 @@ async def websocket_endpoint(
if compiled_references: if compiled_references:
headings = "\n- " + "\n- ".join(set([c.get("compiled", c).split("\n")[0] for c in compiled_references])) headings = "\n- " + "\n- ".join(set([c.get("compiled", c).split("\n")[0] for c in compiled_references]))
await send_status_update(f"**📜 Found Relevant Notes**: {headings}") await send_status_update(f"**Found Relevant Notes**: {headings}")
online_results: Dict = dict() online_results: Dict = dict()
@@ -811,7 +798,7 @@ async def websocket_endpoint(
for webpage in direct_web_pages[query]["webpages"]: for webpage in direct_web_pages[query]["webpages"]:
webpages.append(webpage["link"]) webpages.append(webpage["link"])
await send_status_update(f"**📚 Read web pages**: {webpages}") await send_status_update(f"**Read web pages**: {webpages}")
except ValueError as e: except ValueError as e:
logger.warning( logger.warning(
f"Error directly reading webpages: {e}. Attempting to respond without online results", exc_info=True f"Error directly reading webpages: {e}. Attempting to respond without online results", exc_info=True
@@ -861,7 +848,7 @@ async def websocket_endpoint(
await send_complete_llm_response(json.dumps(content_obj)) await send_complete_llm_response(json.dumps(content_obj))
continue continue
await send_status_update(f"**💭 Generating a well-informed response**") await send_status_update(f"**Generating a well-informed response**")
llm_response, chat_metadata = await agenerate_chat_response( llm_response, chat_metadata = await agenerate_chat_response(
defiltered_query, defiltered_query,
meta_log, meta_log,

View File

@@ -27,11 +27,16 @@ class LocationData(BaseModel):
country: Optional[str] country: Optional[str]
class FilterRequest(BaseModel): class FileFilterRequest(BaseModel):
filename: str filename: str
conversation_id: str conversation_id: str
class FilesFilterRequest(BaseModel):
filenames: List[str]
conversation_id: str
class TextConfigBase(ConfigBase): class TextConfigBase(ConfigBase):
compressed_jsonl: Path compressed_jsonl: Path
embeddings_file: Path embeddings_file: Path