mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-08 21:29:12 +00:00
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:
@@ -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} />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 += ``;
|
||||||
|
} else if (imageJson.intentType === "text-to-image2") {
|
||||||
|
rawResponse += ``;
|
||||||
|
} else if (imageJson.intentType === "text-to-image-v3") {
|
||||||
|
rawResponse = ``;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
17
src/interface/web/app/common/utils.ts
Normal file
17
src/interface/web/app/common/utils.ts
Normal 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
|
||||||
|
`);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
div.actualInputArea {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto auto;
|
||||||
|
}
|
||||||
318
src/interface/web/app/components/chatInputArea/chatInputArea.tsx
Normal file
318
src/interface/web/app/components/chatInputArea/chatInputArea.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
22
src/interface/web/app/components/loading/loading.tsx
Normal file
22
src/interface/web/app/components/loading/loading.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
42
src/interface/web/app/components/loginPrompt/loginPrompt.tsx
Normal file
42
src/interface/web/app/components/loginPrompt/loginPrompt.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/interface/web/app/share/chat/layout.tsx
Normal file
33
src/interface/web/app/share/chat/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
324
src/interface/web/app/share/chat/page.tsx
Normal file
324
src/interface/web/app/share/chat/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
127
src/interface/web/app/share/chat/sharedChat.module.css
Normal file
127
src/interface/web/app/share/chat/sharedChat.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
50
src/interface/web/components/ui/avatar.tsx
Normal file
50
src/interface/web/components/ui/avatar.tsx
Normal 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 }
|
||||||
11
src/interface/web/components/ui/collapsible.tsx
Normal file
11
src/interface/web/components/ui/collapsible.tsx
Normal 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 }
|
||||||
155
src/interface/web/components/ui/command.tsx
Normal file
155
src/interface/web/components/ui/command.tsx
Normal 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,
|
||||||
|
}
|
||||||
118
src/interface/web/components/ui/drawer.tsx
Normal file
118
src/interface/web/components/ui/drawer.tsx
Normal 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,
|
||||||
|
}
|
||||||
200
src/interface/web/components/ui/dropdown-menu.tsx
Normal file
200
src/interface/web/components/ui/dropdown-menu.tsx
Normal 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,
|
||||||
|
}
|
||||||
236
src/interface/web/components/ui/menubar.tsx
Normal file
236
src/interface/web/components/ui/menubar.tsx
Normal 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,
|
||||||
|
}
|
||||||
128
src/interface/web/components/ui/navigation-menu.tsx
Normal file
128
src/interface/web/components/ui/navigation-menu.tsx
Normal 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,
|
||||||
|
}
|
||||||
31
src/interface/web/components/ui/popover.tsx
Normal file
31
src/interface/web/components/ui/popover.tsx
Normal 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 }
|
||||||
32
src/interface/web/components/ui/progress.tsx
Normal file
32
src/interface/web/components/ui/progress.tsx
Normal 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 }
|
||||||
48
src/interface/web/components/ui/scroll-area.tsx
Normal file
48
src/interface/web/components/ui/scroll-area.tsx
Normal 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 }
|
||||||
140
src/interface/web/components/ui/sheet.tsx
Normal file
140
src/interface/web/components/ui/sheet.tsx
Normal 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,
|
||||||
|
}
|
||||||
24
src/interface/web/components/ui/textarea.tsx
Normal file
24
src/interface/web/components/ui/textarea.tsx
Normal 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 }
|
||||||
45
src/interface/web/components/ui/toggle.tsx
Normal file
45
src/interface/web/components/ui/toggle.tsx
Normal 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 }
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user