mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-09 05:39:12 +00:00
Checkpoint: Updated sidebar panel with new components
- Add non-functional UI elements for chat, references, feedback buttons, rename/share session, mic, attachment, websocket connection
This commit is contained in:
@@ -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} />
|
||||||
|
|||||||
@@ -12,23 +12,27 @@ div.main {
|
|||||||
|
|
||||||
div.inputBox {
|
div.inputBox {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: auto 1fr auto auto;
|
||||||
padding: 1rem;
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 1rem;
|
border-radius: 16px;
|
||||||
background-color: #f5f5f5;
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03);
|
||||||
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.1);
|
margin-bottom: 20px;
|
||||||
|
gap: 12px;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.inputBox {
|
input.inputBox {
|
||||||
border: none;
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.inputBox:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.inputBox:focus {
|
div.inputBox:focus {
|
||||||
border: none;
|
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||||
outline: none;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.chatBodyFull {
|
div.chatBodyFull {
|
||||||
@@ -65,9 +69,7 @@ div.chatLayout {
|
|||||||
|
|
||||||
div.chatBox {
|
div.chatBox {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.titleBar {
|
div.titleBar {
|
||||||
@@ -75,6 +77,25 @@ div.titleBar {
|
|||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.chatBoxBody {
|
||||||
|
display: grid;
|
||||||
|
height: 100%;
|
||||||
|
width: 70%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.agentIndicator a {
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.agentIndicator {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
div.chatBody {
|
div.chatBody {
|
||||||
grid-template-columns: 0fr 1fr;
|
grid-template-columns: 0fr 1fr;
|
||||||
@@ -84,3 +105,22 @@ div.titleBar {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
div.inputBox {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBoxBody {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBox {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatLayout {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,54 +6,69 @@ import React, { Suspense, useEffect, useState } from 'react';
|
|||||||
import SuggestionCard from '../components/suggestions/suggestionCard';
|
import SuggestionCard from '../components/suggestions/suggestionCard';
|
||||||
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
||||||
import ChatHistory from '../components/chatHistory/chatHistory';
|
import ChatHistory from '../components/chatHistory/chatHistory';
|
||||||
import { SingleChatMessage } from '../components/chatMessage/chatMessage';
|
|
||||||
import NavMenu from '../components/navMenu/navMenu';
|
import NavMenu from '../components/navMenu/navMenu';
|
||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
import ReferencePanel, { hasValidReferences } from '../components/referencePanel/referencePanel';
|
import Loading from '../components/loading/loading';
|
||||||
|
|
||||||
|
import { setupWebSocket } from '../common/chatFunctions';
|
||||||
|
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
|
import { Lightbulb, ArrowCircleUp, FileArrowUp, Microphone } from '@phosphor-icons/react';
|
||||||
|
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
export function TextareaWithLabel() {
|
||||||
|
return (
|
||||||
|
<div className="grid w-full gap-1.5">
|
||||||
|
{/* <Label htmlFor="message">Your message</Label> */}
|
||||||
|
<Textarea className='border-none min-h-[60px]' placeholder="Type / to see a list of commands" id="message" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface ChatOptions {
|
interface ChatOptions {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple'];
|
const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple'];
|
||||||
|
|
||||||
|
interface ChatBodyDataProps {
|
||||||
|
chatOptionsData: ChatOptions | null;
|
||||||
|
setTitle: (title: string) => void;
|
||||||
|
setConversationID?: (conversationId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
function ChatBodyData({ chatOptionsData }: { chatOptionsData: ChatOptions | null }) {
|
|
||||||
const searchParams = useSearchParams();
|
function ChatBodyData(props: ChatBodyDataProps) {
|
||||||
const conversationId = searchParams.get('conversationId');
|
const searchParams = useSearchParams();
|
||||||
const [showReferencePanel, setShowReferencePanel] = useState(true);
|
const conversationId = searchParams.get('conversationId');
|
||||||
const [referencePanelData, setReferencePanelData] = useState<SingleChatMessage | null>(null);
|
|
||||||
|
if (conversationId && props.setConversationID) {
|
||||||
|
props.setConversationID(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
if (!conversationId) {
|
if (!conversationId) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.suggestions}>
|
<div className={styles.suggestions}>
|
||||||
{chatOptionsData && Object.entries(chatOptionsData).map(([key, value]) => (
|
{props.chatOptionsData && Object.entries(props.chatOptionsData).map(([key, value]) => (
|
||||||
<SuggestionCard
|
<SuggestionCard
|
||||||
key={key}
|
key={key}
|
||||||
title={`/${key}`}
|
title={`/${key}`}
|
||||||
body={value}
|
body={value}
|
||||||
link='#' // replace with actual link if available
|
link='#' // replace with actual link if available
|
||||||
styleClass={styleClassOptions[Math.floor(Math.random() * styleClassOptions.length)]}
|
styleClass={styleClassOptions[Math.floor(Math.random() * styleClassOptions.length)]}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return(
|
return (
|
||||||
<div className={(hasValidReferences(referencePanelData) && showReferencePanel) ? styles.chatBody : styles.chatBodyFull}>
|
<div className={false ? styles.chatBody : styles.chatBodyFull}>
|
||||||
<ChatHistory conversationId={conversationId} setReferencePanelData={setReferencePanelData} setShowReferencePanel={setShowReferencePanel} />
|
<ChatHistory conversationId={conversationId} setTitle={props.setTitle} />
|
||||||
{
|
|
||||||
(hasValidReferences(referencePanelData) && showReferencePanel) &&
|
|
||||||
<ReferencePanel referencePanelData={referencePanelData} setShowReferencePanel={setShowReferencePanel} />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
function Loading() {
|
|
||||||
return <h2>🌀 Loading...</h2>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChatInput(e: React.FormEvent<HTMLInputElement>) {
|
function handleChatInput(e: React.FormEvent<HTMLInputElement>) {
|
||||||
@@ -63,9 +78,12 @@ function handleChatInput(e: React.FormEvent<HTMLInputElement>) {
|
|||||||
|
|
||||||
export default function Chat() {
|
export default function Chat() {
|
||||||
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
|
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
|
||||||
const [isLoading, setLoading] = useState(true)
|
const [isLoading, setLoading] = useState(true);
|
||||||
|
const [title, setTitle] = useState('Chat');
|
||||||
|
const [conversationId, setConversationID] = useState<string | null>(null);
|
||||||
|
const [chatWS, setChatWS] = useState<WebSocket | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/chat/options')
|
fetch('/api/chat/options')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then((data: ChatOptions) => {
|
.then((data: ChatOptions) => {
|
||||||
@@ -80,27 +98,60 @@ export default function Chat() {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
}, []);
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (conversationId) {
|
||||||
|
const newWS = await setupWebSocket(conversationId);
|
||||||
|
setChatWS(newWS);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [conversationId]);
|
||||||
|
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.main + " " + styles.chatLayout}>
|
<div className={styles.main + " " + styles.chatLayout}>
|
||||||
<div className={styles.sidePanel}>
|
<div className={styles.sidePanel}>
|
||||||
<SidePanel />
|
<SidePanel webSocketConnected={chatWS !== null} />
|
||||||
</div>
|
</div>
|
||||||
|
<title>
|
||||||
|
Khoj AI - Chat
|
||||||
|
</title>
|
||||||
<div className={styles.chatBox}>
|
<div className={styles.chatBox}>
|
||||||
<title>
|
<NavMenu selected="Chat" title={title} />
|
||||||
Khoj AI - Chat
|
<div className={styles.chatBoxBody}>
|
||||||
</title>
|
<div>
|
||||||
<NavMenu selected="Chat" />
|
<Suspense fallback={<Loading />}>
|
||||||
<div>
|
<ChatBodyData chatOptionsData={chatOptionsData} setTitle={setTitle} setConversationID={setConversationID} />
|
||||||
<Suspense fallback={<Loading />}>
|
</Suspense>
|
||||||
<ChatBodyData chatOptionsData={chatOptionsData} />
|
</div>
|
||||||
</Suspense>
|
{/* <div className={styles.agentIndicator}>
|
||||||
</div>
|
<a className='no-underline' href="/agents?agent=khoj" target="_blank" rel="noreferrer">
|
||||||
<div className={styles.inputBox}>
|
<Lightbulb color='var(--khoj-orange)' weight='fill' />
|
||||||
<input className={styles.inputBox} type="text" placeholder="Type here..." onInput={(e) => handleChatInput(e)} />
|
<span className='text-neutral-600'>Khoj</span>
|
||||||
<button className={styles.inputBox}>Send</button>
|
</a>
|
||||||
|
</div> */}
|
||||||
|
<div className={`${styles.inputBox} bg-background align-middle items-center justify-center`}>
|
||||||
|
<Button className="!bg-transparent !hover:bg-transparent p-0 h-auto text-3xl">
|
||||||
|
<FileArrowUp fill="hsla(var(--secondary-foreground))"/>
|
||||||
|
</Button>
|
||||||
|
<TextareaWithLabel />
|
||||||
|
<Button className="!bg-transparent !hover:bg-transparent p-0 h-auto text-3xl">
|
||||||
|
<Microphone fill="hsla(var(--secondary-foreground))"/>
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-orange-300 hover:bg-orange-500 rounded-full p-0 h-auto text-3xl">
|
||||||
|
<ArrowCircleUp/>
|
||||||
|
</Button>
|
||||||
|
{/* <input className={styles.inputBox} type="text" placeholder="Type / to see a list of commands" onInput={(e) => handleChatInput(e)} /> */}
|
||||||
|
{/* <button className={styles.inputBox}>Send</button> */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { Context, OnlineContextData } from "../components/chatMessage/chatMessage";
|
||||||
|
|
||||||
|
interface ResponseWithReferences {
|
||||||
|
context?: Context[];
|
||||||
|
online?: {
|
||||||
|
[key: string]: OnlineContextData
|
||||||
|
}
|
||||||
|
response?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCompiledReferences(chunk: string, currentResponse: string) {
|
||||||
|
const rawReference = chunk.split("### compiled references:")[1];
|
||||||
|
const rawResponse = chunk.split("### compiled references:")[0];
|
||||||
|
let references: ResponseWithReferences = {};
|
||||||
|
|
||||||
|
// Set the initial response
|
||||||
|
references.response = currentResponse + rawResponse;
|
||||||
|
|
||||||
|
const rawReferenceAsJson = JSON.parse(rawReference);
|
||||||
|
if (rawReferenceAsJson instanceof Array) {
|
||||||
|
references.context = rawReferenceAsJson;
|
||||||
|
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
|
||||||
|
references.online = rawReferenceAsJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
return references;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendChatStream(
|
||||||
|
message: string,
|
||||||
|
conversationId: string,
|
||||||
|
setIsLoading: (loading: boolean) => void,
|
||||||
|
setInitialResponse: (response: string) => void,
|
||||||
|
setInitialReferences: (references: ResponseWithReferences) => void) {
|
||||||
|
setIsLoading(true);
|
||||||
|
// Send a message to the chat server to verify the fact
|
||||||
|
const chatURL = "/api/chat";
|
||||||
|
const apiURL = `${chatURL}?q=${encodeURIComponent(message)}&client=web&stream=true&conversation_id=${conversationId}`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiURL);
|
||||||
|
if (!response.body) throw new Error("No response body found");
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
let decoder = new TextDecoder();
|
||||||
|
let result = "";
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
let chunk = decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
if (chunk.includes("### compiled references:")) {
|
||||||
|
const references = handleCompiledReferences(chunk, result);
|
||||||
|
if (references.response) {
|
||||||
|
result = references.response;
|
||||||
|
setInitialResponse(references.response);
|
||||||
|
setInitialReferences(references);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result += chunk;
|
||||||
|
setInitialResponse(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error verifying statement: ", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendChatWS(websocket: WebSocket, message: string) {
|
||||||
|
websocket.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setupWebSocket = async (conversationId: string) => {
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
|
||||||
|
const host = process.env.NODE_ENV === 'production' ? window.location.host : 'localhost:42110';
|
||||||
|
|
||||||
|
let webSocketUrl = `${wsProtocol}//${host}/api/chat/ws`;
|
||||||
|
|
||||||
|
if (conversationId === null) return null;
|
||||||
|
|
||||||
|
if (conversationId) {
|
||||||
|
webSocketUrl += `?conversation_id=${conversationId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("WebSocket URL: ", webSocketUrl);
|
||||||
|
|
||||||
|
const chatWS = new WebSocket(webSocketUrl);
|
||||||
|
|
||||||
|
chatWS.onopen = () => {
|
||||||
|
console.log('WebSocket connection established');
|
||||||
|
};
|
||||||
|
|
||||||
|
chatWS.onmessage = (event) => {
|
||||||
|
console.log(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
chatWS.onerror = (error) => {
|
||||||
|
console.error('WebSocket error: ', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
chatWS.onclose = () => {
|
||||||
|
console.log('WebSocket connection closed');
|
||||||
|
};
|
||||||
|
|
||||||
|
return chatWS;
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,6 +7,16 @@ div.chatHistory {
|
|||||||
div.chatLayout {
|
div.chatLayout {
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
/* width: 80%; */
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.agentIndicator a {
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.agentIndicator {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,18 @@ import { useRef, useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import ChatMessage, { ChatHistoryData, SingleChatMessage } from '../chatMessage/chatMessage';
|
import ChatMessage, { ChatHistoryData, SingleChatMessage } from '../chatMessage/chatMessage';
|
||||||
|
|
||||||
|
import ReferencePanel, { hasValidReferences} from '../referencePanel/referencePanel';
|
||||||
|
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
|
||||||
import renderMathInElement from 'katex/contrib/auto-render';
|
import renderMathInElement from 'katex/contrib/auto-render';
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
import 'highlight.js/styles/github.css'
|
import 'highlight.js/styles/github.css'
|
||||||
|
|
||||||
|
import Loading from '../loading/loading';
|
||||||
|
|
||||||
|
import { Lightbulb } from "@phosphor-icons/react";
|
||||||
|
|
||||||
interface ChatResponse {
|
interface ChatResponse {
|
||||||
status: string;
|
status: string;
|
||||||
response: ChatHistoryData;
|
response: ChatHistoryData;
|
||||||
@@ -20,8 +28,7 @@ interface ChatHistory {
|
|||||||
|
|
||||||
interface ChatHistoryProps {
|
interface ChatHistoryProps {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
setReferencePanelData: Function;
|
setTitle: (title: string) => void;
|
||||||
setShowReferencePanel: Function;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -31,6 +38,8 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const chatHistoryRef = useRef(null);
|
const chatHistoryRef = useRef(null);
|
||||||
|
|
||||||
|
const [showReferencePanel, setShowReferencePanel] = useState(true);
|
||||||
|
const [referencePanelData, setReferencePanelData] = useState<SingleChatMessage | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
@@ -38,10 +47,12 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then((chatData: ChatResponse) => {
|
.then((chatData: ChatResponse) => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
// Render chat options, if any
|
// Render chat options, if any
|
||||||
if (chatData) {
|
if (chatData) {
|
||||||
console.log(chatData);
|
console.log(chatData);
|
||||||
setData(chatData.response);
|
setData(chatData.response);
|
||||||
|
props.setTitle(chatData.response.slug);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@@ -55,7 +66,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
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: [
|
||||||
@@ -78,23 +89,50 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <h2>🌀 Loading...</h2>;
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructAgentLink() {
|
||||||
|
if (!data || !data.agent || !data.agent.slug) return `/agents`;
|
||||||
|
return `/agents?agent=${data.agent.slug}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructAgentAvatar() {
|
||||||
|
if (!data || !data.agent || !data.agent.avatar) return `/avatar.png`;
|
||||||
|
return data.agent.avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructAgentName() {
|
||||||
|
if (!data || !data.agent || !data.agent.name) return `Agent`;
|
||||||
|
return data.agent.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.main + " " + styles.chatLayout}>
|
<ScrollArea className={`h-[80vh]`}>
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<div className={styles.chatHistory} ref={chatHistoryRef}>
|
<div className={styles.chatHistory} ref={chatHistoryRef}>
|
||||||
{(data && data.chat) && data.chat.map((chatMessage, index) => (
|
{(data && data.chat) && data.chat.map((chatMessage, index) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={index}
|
key={index}
|
||||||
chatMessage={chatMessage}
|
chatMessage={chatMessage}
|
||||||
setReferencePanelData={props.setReferencePanelData}
|
setReferencePanelData={setReferencePanelData}
|
||||||
setShowReferencePanel={props.setShowReferencePanel}
|
setShowReferencePanel={setShowReferencePanel}
|
||||||
|
customClassName='fullHistory'
|
||||||
|
borderLeftColor='orange-400'
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{
|
||||||
|
(hasValidReferences(referencePanelData) && showReferencePanel) &&
|
||||||
|
<ReferencePanel referencePanelData={referencePanelData} setShowReferencePanel={setShowReferencePanel} />
|
||||||
|
}
|
||||||
|
<div className={`${styles.agentIndicator}`}>
|
||||||
|
<a className='no-underline mx-2 flex' href={constructAgentLink()} target="_blank" rel="noreferrer">
|
||||||
|
<Lightbulb color='orange' weight='fill' />
|
||||||
|
<span className='text-neutral-600'>{constructAgentName()}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,34 @@
|
|||||||
div.chatMessageContainer {
|
div.chatMessageContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
margin: 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatMessageWrapper {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.khojfullHistory {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
border-width: 1px;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.youfullHistory {
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatMessageContainer.youfullHistory {
|
||||||
|
padding-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.you {
|
div.you {
|
||||||
color: var(--frosted-background-color);
|
background-color: var(--frosted-background-color);
|
||||||
background-color: var(--intense-green);
|
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.khoj {
|
div.khoj {
|
||||||
@@ -15,6 +37,11 @@ div.khoj {
|
|||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.khojChatMessage {
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
div.chatMessageContainer img {
|
div.chatMessageContainer img {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
@@ -42,21 +69,25 @@ div.chatFooter {
|
|||||||
div.chatButtons {
|
div.chatButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
border: var(--border-color) 1px solid;
|
||||||
|
border-radius: 16px;
|
||||||
|
position: relative;
|
||||||
|
bottom: -28px;
|
||||||
|
background-color: hsla(var(--background));
|
||||||
}
|
}
|
||||||
|
|
||||||
div.chatFooter button {
|
div.chatFooter button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--calm-blue);
|
color: hsl(var(--muted-foreground));
|
||||||
color: var(--main-text-color);
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.5rem;
|
border-radius: 16px;
|
||||||
padding: 0.25rem;
|
padding: 4px;
|
||||||
margin-left: 0.5rem;
|
margin-left: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.chatFooter button:hover {
|
div.chatFooter button:hover {
|
||||||
background-color: var(--frosted-background-color);
|
background-color: var(--frosted-background-color);
|
||||||
color: var(--intense-green);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.chatTimestamp {
|
div.chatTimestamp {
|
||||||
@@ -70,7 +101,6 @@ button.codeCopyButton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button.codeCopyButton:hover {
|
button.codeCopyButton:hover {
|
||||||
background-color: var(--intense-green);
|
|
||||||
color: var(--frosted-background-color);
|
color: var(--frosted-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,3 +108,10 @@ div.feedbackButtons img,
|
|||||||
button.copyButton img {
|
button.copyButton img {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
div.youfullHistory {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import 'highlight.js/styles/github.css'
|
|||||||
|
|
||||||
import { hasValidReferences } from '../referencePanel/referencePanel';
|
import { hasValidReferences } from '../referencePanel/referencePanel';
|
||||||
|
|
||||||
|
import { ThumbsUp, ThumbsDown, Copy } from '@phosphor-icons/react';
|
||||||
|
|
||||||
const md = new markdownIt({
|
const md = new markdownIt({
|
||||||
html: true,
|
html: true,
|
||||||
linkify: true,
|
linkify: true,
|
||||||
@@ -103,31 +105,19 @@ export interface ChatHistoryData {
|
|||||||
|
|
||||||
function FeedbackButtons() {
|
function FeedbackButtons() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.feedbackButtons}>
|
<div className={`${styles.feedbackButtons} flex align-middle justify-center items-center`}>
|
||||||
<button className={styles.thumbsUpButton}>
|
<button className={styles.thumbsUpButton}>
|
||||||
<Image
|
<ThumbsUp color='hsl(var(--muted-foreground))' />
|
||||||
src="/thumbs-up.svg"
|
|
||||||
alt="Thumbs Up"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
<button className={styles.thumbsDownButton}>
|
<button className={styles.thumbsDownButton}>
|
||||||
<Image
|
<ThumbsDown color='hsl(var(--muted-foreground))' />
|
||||||
src="/thumbs-down.svg"
|
|
||||||
alt="Thumbs Down"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClickMessage(event: React.MouseEvent<any>, chatMessage: SingleChatMessage, setReferencePanelData: Function, setShowReferencePanel: Function) {
|
function onClickMessage(event: React.MouseEvent<any>, chatMessage: SingleChatMessage, setReferencePanelData: Function, setShowReferencePanel: Function) {
|
||||||
event.preventDefault();
|
console.log("Clicked on message", chatMessage);
|
||||||
setReferencePanelData(chatMessage);
|
setReferencePanelData(chatMessage);
|
||||||
setShowReferencePanel(true);
|
setShowReferencePanel(true);
|
||||||
}
|
}
|
||||||
@@ -136,6 +126,8 @@ interface ChatMessageProps {
|
|||||||
chatMessage: SingleChatMessage;
|
chatMessage: SingleChatMessage;
|
||||||
setReferencePanelData: Function;
|
setReferencePanelData: Function;
|
||||||
setShowReferencePanel: Function;
|
setShowReferencePanel: Function;
|
||||||
|
customClassName?: string;
|
||||||
|
borderLeftColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatMessage(props: ChatMessageProps) {
|
export default function ChatMessage(props: ChatMessageProps) {
|
||||||
@@ -204,53 +196,65 @@ export default function ChatMessage(props: ChatMessageProps) {
|
|||||||
|
|
||||||
let referencesValid = hasValidReferences(props.chatMessage);
|
let referencesValid = hasValidReferences(props.chatMessage);
|
||||||
|
|
||||||
|
function constructClasses(chatMessage: SingleChatMessage) {
|
||||||
|
let classes = [styles.chatMessageContainer];
|
||||||
|
classes.push(styles[chatMessage.by]);
|
||||||
|
|
||||||
|
if (props.customClassName) {
|
||||||
|
classes.push(styles[`${chatMessage.by}${props.customClassName}`])
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function chatMessageWrapperClasses(chatMessage: SingleChatMessage) {
|
||||||
|
let classes = [styles.chatMessageWrapper];
|
||||||
|
classes.push(styles[chatMessage.by]);
|
||||||
|
|
||||||
|
if (chatMessage.by === "khoj") {
|
||||||
|
classes.push(`border-l-4 border-opacity-50 border-l-orange-500 border-l-${props.borderLeftColor}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${styles.chatMessageContainer} ${styles[props.chatMessage.by]}`}
|
className={constructClasses(props.chatMessage)}
|
||||||
onClick={props.chatMessage.by === "khoj" ? (event) => onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel) : undefined}>
|
onClick={props.chatMessage.by === "khoj" ? (event) => onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel) : undefined}>
|
||||||
{/* <div className={styles.chatFooter}> */}
|
{/* <div className={styles.chatFooter}> */}
|
||||||
{/* {props.chatMessage.by} */}
|
{/* {props.chatMessage.by} */}
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
<div ref={messageRef} className={styles.chatMessage} dangerouslySetInnerHTML={{ __html: markdownRendered }} />
|
<div className={chatMessageWrapperClasses(props.chatMessage)}>
|
||||||
{/* Add a copy button, thumbs up, and thumbs down buttons */}
|
<div ref={messageRef} className={styles.chatMessage} dangerouslySetInnerHTML={{ __html: markdownRendered }} />
|
||||||
<div className={styles.chatFooter}>
|
{/* Add a copy button, thumbs up, and thumbs down buttons */}
|
||||||
<div className={styles.chatTimestamp}>
|
<div className={styles.chatFooter}>
|
||||||
{renderTimeStamp(props.chatMessage.created)}
|
<div className={styles.chatTimestamp}>
|
||||||
</div>
|
{renderTimeStamp(props.chatMessage.created)}
|
||||||
<div className={styles.chatButtons}>
|
</div>
|
||||||
{
|
<div className={styles.chatButtons}>
|
||||||
referencesValid &&
|
|
||||||
<div className={styles.referenceButton}>
|
|
||||||
<button onClick={(event) => onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel)}>
|
|
||||||
References
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<button className={`${styles.copyButton}`} onClick={() => {
|
|
||||||
navigator.clipboard.writeText(props.chatMessage.message);
|
|
||||||
setCopySuccess(true);
|
|
||||||
}}>
|
|
||||||
{
|
{
|
||||||
copySuccess ?
|
referencesValid &&
|
||||||
<Image
|
<div className={styles.referenceButton}>
|
||||||
src="/copy-button-success.svg"
|
<button onClick={(event) => onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel)}>
|
||||||
alt="Checkmark"
|
References
|
||||||
width={24}
|
</button>
|
||||||
height={24}
|
</div>
|
||||||
priority
|
|
||||||
/>
|
|
||||||
: <Image
|
|
||||||
src="/copy-button.svg"
|
|
||||||
alt="Copy"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
</button>
|
<button className={`${styles.copyButton}`} onClick={() => {
|
||||||
{
|
navigator.clipboard.writeText(props.chatMessage.message);
|
||||||
props.chatMessage.by === "khoj" && <FeedbackButtons />
|
setCopySuccess(true);
|
||||||
}
|
}}>
|
||||||
|
{
|
||||||
|
copySuccess ?
|
||||||
|
<Copy color='green' />
|
||||||
|
: <Copy color='hsl(var(--muted-foreground))' />
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
{
|
||||||
|
props.chatMessage.by === "khoj" && <FeedbackButtons />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
24
src/interface/web/app/components/loading/loading.module.css
Normal file
24
src/interface/web/app/components/loading/loading.module.css
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/* HTML: <div class="loader"></div> */
|
||||||
|
.loader {
|
||||||
|
--c: conic-gradient(from -90deg, hsla(var(--secondary)) 90deg, #0000 0);
|
||||||
|
background: var(--c), var(--c);
|
||||||
|
background-size: 40% 40%;
|
||||||
|
animation: l19 1s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes l19 {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
10% {
|
||||||
|
background-position: 0 0, 0 calc(100%/3)
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-position: 0 0, calc(100%/3) calc(100%/3)
|
||||||
|
}
|
||||||
|
|
||||||
|
90%,
|
||||||
|
100% {
|
||||||
|
background-position: 0 0, calc(100%/3) 0
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/interface/web/app/components/loading/loading.tsx
Normal file
26
src/interface/web/app/components/loading/loading.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import styles from './loading.module.css';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
// return (
|
||||||
|
// <div className={`${styles.loading} h-[100vh] flex items-center justify-center`}>
|
||||||
|
// <button type="button" className="bg-indigo-500" disabled>
|
||||||
|
// Loading...
|
||||||
|
// <span className="relative flex h-3 w-3">
|
||||||
|
// <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
|
||||||
|
// <span className="relative inline-flex rounded-full h-3 w-3 bg-sky-500"></span>
|
||||||
|
// </span>
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// )
|
||||||
|
return (
|
||||||
|
<div className={`${styles.loader} h-[100vh] flex items-center justify-center`}></div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-[100vh] flex items-center justify-center">
|
||||||
|
<h2 className="text-4xl text-black animate-bounce">
|
||||||
|
Loading...
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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,125 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import styles from './navMenu.module.css';
|
import styles from './navMenu.module.css';
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useAuthenticatedData, UserProfile } from '@/app/common/auth';
|
import { useAuthenticatedData, UserProfile } from '@/app/common/auth';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Menubar,
|
||||||
|
MenubarContent,
|
||||||
|
MenubarItem,
|
||||||
|
MenubarMenu,
|
||||||
|
MenubarSeparator,
|
||||||
|
MenubarTrigger,
|
||||||
|
} from "@/components/ui/menubar";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
|
||||||
interface NavMenuProps {
|
interface NavMenuProps {
|
||||||
selected: string;
|
selected: string;
|
||||||
|
showLogo?: boolean;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsMenu(props: UserProfile) {
|
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.settingsMenu}>
|
|
||||||
<div className={styles.settingsMenuProfile} onClick={() => setShowSettings(!showSettings)}>
|
|
||||||
<Image
|
|
||||||
src={props.photo || "/agents.svg"}
|
|
||||||
alt={props.username}
|
|
||||||
width={50}
|
|
||||||
height={50}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{showSettings && (
|
|
||||||
<div className={styles.settingsMenuOptions}>
|
|
||||||
<div className={styles.settingsMenuUsername}>{props.username}</div>
|
|
||||||
<Link href="/config">
|
|
||||||
Settings
|
|
||||||
</Link>
|
|
||||||
<Link href="https://github.com/khoj-ai/khoj">
|
|
||||||
Github
|
|
||||||
</Link>
|
|
||||||
<Link href="https://docs.khoj.dev">
|
|
||||||
Help
|
|
||||||
</Link>
|
|
||||||
<Link href="/auth/logout">
|
|
||||||
Logout
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default function NavMenu(props: NavMenuProps) {
|
export default function NavMenu(props: NavMenuProps) {
|
||||||
|
|
||||||
let userData = useAuthenticatedData();
|
const userData = useAuthenticatedData();
|
||||||
|
const [displayTitle, setDisplayTitle] = useState<string>(props.title || props.selected.toUpperCase());
|
||||||
|
|
||||||
|
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMobileWidth(window.innerWidth < 768);
|
||||||
|
setDisplayTitle(props.title || props.selected.toUpperCase());
|
||||||
|
|
||||||
|
}, [props.title]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.titleBar}>
|
<div className={styles.titleBar}>
|
||||||
<Link href="/">
|
<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="/khoj-logo.svg"
|
</div>
|
||||||
alt="Khoj Logo"
|
{
|
||||||
className={styles.logo}
|
isMobileWidth ?
|
||||||
width={100}
|
<DropdownMenu>
|
||||||
height={50}
|
<DropdownMenuTrigger>=</DropdownMenuTrigger>
|
||||||
priority
|
<DropdownMenuContent>
|
||||||
/>
|
<DropdownMenuItem>
|
||||||
</Link>
|
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>Chat</Link>
|
||||||
<menu className={styles.menu}>
|
</DropdownMenuItem>
|
||||||
<a className={props.selected === "Chat" ? styles.selected : ""} href = '/chat'>
|
<DropdownMenuItem>
|
||||||
<Image
|
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>Agents</Link>
|
||||||
src="/chat.svg"
|
</DropdownMenuItem>
|
||||||
alt="Chat Logo"
|
<DropdownMenuItem>
|
||||||
className={styles.lgoo}
|
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>Automations</Link>
|
||||||
width={24}
|
</DropdownMenuItem>
|
||||||
height={24}
|
{userData && <>
|
||||||
priority
|
<DropdownMenuSeparator />
|
||||||
/>
|
<DropdownMenuLabel>Profile</DropdownMenuLabel>
|
||||||
<span>
|
<DropdownMenuItem>
|
||||||
Chat
|
<Link href="/config">Settings</Link>
|
||||||
</span>
|
</DropdownMenuItem>
|
||||||
</a>
|
<DropdownMenuItem>
|
||||||
<a className={props.selected === "Agents" ? styles.selected : ""} href='/agents'>
|
<Link href="https://docs.khoj.dev">Help</Link>
|
||||||
<Image
|
</DropdownMenuItem>
|
||||||
src="/agents.svg"
|
<DropdownMenuItem>
|
||||||
alt="Agent Logo"
|
<Link href="/auth/logout">Logout</Link>
|
||||||
className={styles.lgoo}
|
</DropdownMenuItem>
|
||||||
width={24}
|
</>}
|
||||||
height={24}
|
</DropdownMenuContent>
|
||||||
priority
|
</DropdownMenu>
|
||||||
/>
|
:
|
||||||
<span>
|
<Menubar className='items-top'>
|
||||||
Agents
|
<MenubarMenu>
|
||||||
</span>
|
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>
|
||||||
</a>
|
<MenubarTrigger>Chat</MenubarTrigger>
|
||||||
<a className={props.selected === "Automations" ? styles.selected : ""} href = '/automations'>
|
</Link>
|
||||||
<Image
|
</MenubarMenu>
|
||||||
src="/automation.svg"
|
<MenubarMenu>
|
||||||
alt="Automation Logo"
|
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>
|
||||||
className={styles.lgoo}
|
<MenubarTrigger>Agents</MenubarTrigger>
|
||||||
width={24}
|
</Link>
|
||||||
height={24}
|
</MenubarMenu>
|
||||||
priority
|
<MenubarMenu>
|
||||||
/>
|
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>
|
||||||
<span>
|
<MenubarTrigger>Automations</MenubarTrigger>
|
||||||
Automations
|
</Link>
|
||||||
</span>
|
</MenubarMenu>
|
||||||
</a>
|
{userData &&
|
||||||
{userData && <SettingsMenu {...userData} />}
|
<MenubarMenu>
|
||||||
</menu>
|
<MenubarTrigger>Profile</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarItem>
|
||||||
|
<Link href="/config">
|
||||||
|
Settings
|
||||||
|
</Link>
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarItem>
|
||||||
|
<Link href="https://docs.khoj.dev">
|
||||||
|
Help
|
||||||
|
</Link>
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarItem>
|
||||||
|
<Link href="/auth/logout">
|
||||||
|
Logout
|
||||||
|
</Link>
|
||||||
|
</MenubarItem>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
}
|
||||||
|
</Menubar>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ div.panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.panel a {
|
div.panel a {
|
||||||
color: var(--intense-green);
|
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,3 +28,13 @@ div.singleReference {
|
|||||||
background-color: var(--frosted-background-color);
|
background-color: var(--frosted-background-color);
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
div.panel {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.singleReference {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function hasValidReferences(referencePanelData: SingleChatMessage | null)
|
|||||||
(referencePanelData.onlineContext && Object.keys(referencePanelData.onlineContext).length > 0 &&
|
(referencePanelData.onlineContext && Object.keys(referencePanelData.onlineContext).length > 0 &&
|
||||||
Object.values(referencePanelData.onlineContext).some(
|
Object.values(referencePanelData.onlineContext).some(
|
||||||
(onlineContextData) =>
|
(onlineContextData) =>
|
||||||
(onlineContextData.webpages && onlineContextData.webpages.length > 0)|| onlineContextData.answerBox || onlineContextData.peopleAlsoAsk || onlineContextData.knowledgeGraph))
|
(onlineContextData.webpages && onlineContextData.webpages.length > 0) || onlineContextData.answerBox || onlineContextData.peopleAlsoAsk || onlineContextData.knowledgeGraph || onlineContextData.organic ))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -98,6 +98,7 @@ function OnlineReferences(props: { onlineContext: OnlineContextData, query: stri
|
|||||||
const answerBox = props.onlineContext.answerBox;
|
const answerBox = props.onlineContext.answerBox;
|
||||||
const peopleAlsoAsk = props.onlineContext.peopleAlsoAsk;
|
const peopleAlsoAsk = props.onlineContext.peopleAlsoAsk;
|
||||||
const knowledgeGraph = props.onlineContext.knowledgeGraph;
|
const knowledgeGraph = props.onlineContext.knowledgeGraph;
|
||||||
|
const organic = props.onlineContext.organic;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.singleReference}>
|
<div className={styles.singleReference}>
|
||||||
@@ -126,6 +127,24 @@ function OnlineReferences(props: { onlineContext: OnlineContextData, query: stri
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
organic && organic.map((organicData, index) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.onlineReference} key={index}>
|
||||||
|
<div className={styles.onlineReferenceTitle}>
|
||||||
|
<a href={organicData.link} target="_blank" rel="noreferrer">
|
||||||
|
{organicData.title}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className={styles.onlineReferenceContent}>
|
||||||
|
<div>
|
||||||
|
{organicData.snippet}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
{
|
{
|
||||||
peopleAlsoAsk && peopleAlsoAsk.map((people, index) => {
|
peopleAlsoAsk && peopleAlsoAsk.map((people, index) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,69 +5,254 @@ import styles from "./sidePanel.module.css";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { UserProfile } from "@/app/common/auth";
|
import { UserProfile } from "@/app/common/auth";
|
||||||
|
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
interface ChatHistory {
|
interface ChatHistory {
|
||||||
conversation_id: string;
|
conversation_id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
agent_name: string;
|
||||||
|
agent_avatar: string;
|
||||||
|
compressed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatSession(prop: ChatHistory) {
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
import { Pencil, Trash, Share } from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface GroupedChatHistory {
|
||||||
|
[key: string]: ChatHistory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameConversation(conversationId: string, newTitle: string) {
|
||||||
|
const editUrl = `/api/chat/title?client=web&conversation_id=${conversationId}&title=${newTitle}`;
|
||||||
|
|
||||||
|
fetch(editUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log(data);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareConversation(conversationId: string) {
|
||||||
|
const shareUrl = `/api/chat/share?client=web&conversation_id=${conversationId}`;
|
||||||
|
|
||||||
|
fetch(shareUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log(data);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteConversation(conversationId: string) {
|
||||||
|
const deleteUrl = `/api/chat/delete?client=web&conversation_id=${conversationId}`;
|
||||||
|
|
||||||
|
fetch(deleteUrl, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log(data);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatSessionActionMenuProps {
|
||||||
|
conversationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={prop.conversation_id} className={styles.session}>
|
<DropdownMenu>
|
||||||
<Link href={`/chat?conversationId=${prop.conversation_id}`}>
|
<DropdownMenuTrigger>:</DropdownMenuTrigger>
|
||||||
<p className={styles.session}>{prop.slug || "New Conversation 🌱"}</p>
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => renameConversation(props.conversationId, 'New Title')}>
|
||||||
|
<Pencil className="mr-2 h-4 w-4" />Rename
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => shareConversation(props.conversationId)}>
|
||||||
|
<Share className="mr-2 h-4 w-4" />Share
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Button className="p-0 text-sm h-auto text-rose-300 hover:text-rose-400" variant={'ghost'} onClick={() => deleteConversation(props.conversationId)}>
|
||||||
|
<Trash className="mr-2 h-4 w-4" />Delete
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatSession(props: ChatHistory) {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
key={props.conversation_id}
|
||||||
|
className={`${styles.session} ${props.compressed ? styles.compressed : '!max-w-full'} ${isHovered ? `${styles.sessionHover}` : ''}`}>
|
||||||
|
<Link href={`/chat?conversationId=${props.conversation_id}`}>
|
||||||
|
<p className={styles.session}>{props.slug || "New Conversation 🌱"}</p>
|
||||||
</Link>
|
</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-900 cursor-pointer">
|
||||||
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} />
|
Show All
|
||||||
))}
|
</DialogTrigger>
|
||||||
<button className={styles.showMoreButton} onClick={() => setIsExpanded(false)}>
|
<DialogContent>
|
||||||
Close
|
<DialogHeader>
|
||||||
</button>
|
<DialogTitle>All Conversations</DialogTitle>
|
||||||
</div>
|
<DialogDescription>
|
||||||
</div>
|
<ScrollArea className="h-[500px] w-[450px] rounded-md border p-4">
|
||||||
|
{data && Object.keys(data).map((agentName) => (
|
||||||
|
<div key={agentName}>
|
||||||
|
<h3 className={`grid grid-flow-col auto-cols-max gap-2`}>
|
||||||
|
<img src={data[agentName][0].agent_avatar} alt={agentName} width={24} height={24} />
|
||||||
|
{agentName}
|
||||||
|
</h3>
|
||||||
|
{data[agentName].map((chatHistory) => (
|
||||||
|
<ChatSession
|
||||||
|
compressed={false}
|
||||||
|
key={chatHistory.conversation_id}
|
||||||
|
conversation_id={chatHistory.conversation_id}
|
||||||
|
slug={chatHistory.slug}
|
||||||
|
agent_avatar={chatHistory.agent_avatar}
|
||||||
|
agent_name={chatHistory.agent_name} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ScrollArea>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SidePanel() {
|
const fetchChatHistory = async (url: string) => {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useChatHistoryRecentFetchRequest = (url: string) => {
|
||||||
|
const { data, error } = useSWR<ChatHistory[]>(url, fetchChatHistory);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
isLoading: !error && !data,
|
||||||
|
isError: error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SidePanelProps {
|
||||||
|
webSocketConnected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function SidePanel(props: SidePanelProps) {
|
||||||
|
|
||||||
const [data, setData] = useState<ChatHistory[] | null>(null);
|
const [data, setData] = useState<ChatHistory[] | null>(null);
|
||||||
const [dataToShow, setDataToShow] = useState<ChatHistory[] | null>(null);
|
const [organizedData, setOrganizedData] = useState<GroupedChatHistory | null>(null);
|
||||||
|
const [subsetOrganizedData, setSubsetOrganizedData] = useState<GroupedChatHistory | null>(null);
|
||||||
const [isLoading, setLoading] = useState(true)
|
const [isLoading, setLoading] = useState(true)
|
||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: chatHistory } = useChatHistoryRecentFetchRequest('/api/chat/sessions');
|
||||||
|
|
||||||
fetch('/api/chat/sessions', { method: 'GET' })
|
useEffect(() => {
|
||||||
.then(response => response.json())
|
if (chatHistory) {
|
||||||
.then((data: ChatHistory[]) => {
|
setData(chatHistory);
|
||||||
setLoading(false);
|
|
||||||
// Render chat options, if any
|
const groupedData: GroupedChatHistory = {};
|
||||||
if (data) {
|
const subsetOrganizedData: GroupedChatHistory = {};
|
||||||
setData(data);
|
let numAdded = 0;
|
||||||
setDataToShow(data.slice(0, 5));
|
chatHistory.forEach((chatHistory) => {
|
||||||
|
if (!groupedData[chatHistory.agent_name]) {
|
||||||
|
groupedData[chatHistory.agent_name] = [];
|
||||||
|
}
|
||||||
|
groupedData[chatHistory.agent_name].push(chatHistory);
|
||||||
|
|
||||||
|
// Add to subsetOrganizedData if less than 8
|
||||||
|
if (numAdded < 8) {
|
||||||
|
if (!subsetOrganizedData[chatHistory.agent_name]) {
|
||||||
|
subsetOrganizedData[chatHistory.agent_name] = [];
|
||||||
|
}
|
||||||
|
subsetOrganizedData[chatHistory.agent_name].push(chatHistory);
|
||||||
|
numAdded++;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
setSubsetOrganizedData(subsetOrganizedData);
|
||||||
|
setOrganizedData(groupedData);
|
||||||
|
}
|
||||||
|
}, [chatHistory]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
fetch('/api/v1/user', { method: 'GET' })
|
fetch('/api/v1/user', { method: 'GET' })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@@ -78,66 +263,79 @@ export default function SidePanel() {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.panel}`}>
|
<div className={`${styles.panel}`}>
|
||||||
{
|
{
|
||||||
enabled ?
|
enabled ?
|
||||||
<div>
|
<div className={`${styles.panelWrapper}`}>
|
||||||
<div className={`${styles.expanded}`}>
|
<div className={`${styles.expanded}`}>
|
||||||
<div className={`${styles.profile}`}>
|
|
||||||
{ userProfile &&
|
|
||||||
<div className={styles.profile}>
|
|
||||||
<img
|
|
||||||
className={styles.profile}
|
|
||||||
src={userProfile.photo}
|
|
||||||
alt="profile"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
<p>{userProfile?.username}</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<button className={styles.button} onClick={() => setEnabled(false)}>
|
<button className={styles.button} onClick={() => setEnabled(false)}>
|
||||||
{/* Push Close Icon */}
|
{/* Push Close Icon */}
|
||||||
<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M8.70710678,12 L19.5,12 C19.7761424,12 20,12.2238576 20,12.5 C20,12.7761424 19.7761424,13 19.5,13 L8.70710678,13 L11.8535534,16.1464466 C12.0488155,16.3417088 12.0488155,16.6582912 11.8535534,16.8535534 C11.6582912,17.0488155 11.3417088,17.0488155 11.1464466,16.8535534 L7.14644661,12.8535534 C6.95118446,12.6582912 6.95118446,12.3417088 7.14644661,12.1464466 L11.1464466,8.14644661 C11.3417088,7.95118446 11.6582912,7.95118446 11.8535534,8.14644661 C12.0488155,8.34170876 12.0488155,8.65829124 11.8535534,8.85355339 L8.70710678,12 L8.70710678,12 Z M4,5.5 C4,5.22385763 4.22385763,5 4.5,5 C4.77614237,5 5,5.22385763 5,5.5 L5,19.5 C5,19.7761424 4.77614237,20 4.5,20 C4.22385763,20 4,19.7761424 4,19.5 L4,5.5 Z"></path> </g></svg>
|
<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M8.70710678,12 L19.5,12 C19.7761424,12 20,12.2238576 20,12.5 C20,12.7761424 19.7761424,13 19.5,13 L8.70710678,13 L11.8535534,16.1464466 C12.0488155,16.3417088 12.0488155,16.6582912 11.8535534,16.8535534 C11.6582912,17.0488155 11.3417088,17.0488155 11.1464466,16.8535534 L7.14644661,12.8535534 C6.95118446,12.6582912 6.95118446,12.3417088 7.14644661,12.1464466 L11.1464466,8.14644661 C11.3417088,7.95118446 11.6582912,7.95118446 11.8535534,8.14644661 C12.0488155,8.34170876 12.0488155,8.65829124 11.8535534,8.85355339 L8.70710678,12 L8.70710678,12 Z M4,5.5 C4,5.22385763 4.22385763,5 4.5,5 C4.77614237,5 5,5.22385763 5,5.5 L5,19.5 C5,19.7761424 4.77614237,20 4.5,20 C4.22385763,20 4,19.7761424 4,19.5 L4,5.5 Z"></path> </g></svg>
|
||||||
</button>
|
</button>
|
||||||
<h3>Recent Conversations</h3>
|
|
||||||
</div>
|
|
||||||
<div className={styles.sessionsList}>
|
|
||||||
{dataToShow && dataToShow.map((chatHistory) => (
|
|
||||||
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
<ScrollArea className="h-[40vh] w-[14rem]">
|
||||||
|
<div className={styles.sessionsList}>
|
||||||
|
{subsetOrganizedData && Object.keys(subsetOrganizedData).map((agentName) => (
|
||||||
|
<div key={agentName} className={`my-4`}>
|
||||||
|
<h3 className={`grid grid-flow-col auto-cols-max gap-2 my-4 font-bold text-sm`}>
|
||||||
|
<img src={subsetOrganizedData[agentName][0].agent_avatar} alt={agentName} width={24} height={24} />
|
||||||
|
{agentName}
|
||||||
|
</h3>
|
||||||
|
{subsetOrganizedData[agentName].map((chatHistory) => (
|
||||||
|
<ChatSession
|
||||||
|
compressed={true}
|
||||||
|
key={chatHistory.conversation_id}
|
||||||
|
conversation_id={chatHistory.conversation_id}
|
||||||
|
slug={chatHistory.slug}
|
||||||
|
agent_avatar={chatHistory.agent_avatar}
|
||||||
|
agent_name={chatHistory.agent_name} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
{
|
{
|
||||||
(data && data.length > 5) && (
|
(data && data.length > 5) && (
|
||||||
(isExpanded) ?
|
<ChatSessionsModal data={organizedData} />
|
||||||
<ChatSessionsModal data={data} setIsExpanded={setIsExpanded} />
|
|
||||||
:
|
|
||||||
<button className={styles.showMoreButton} onClick={() => {
|
|
||||||
setIsExpanded(true);
|
|
||||||
}}>
|
|
||||||
Show All
|
|
||||||
</button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{userProfile &&
|
||||||
|
<div className={styles.profile}>
|
||||||
|
<Avatar>
|
||||||
|
<AvatarImage src={userProfile.photo} alt="user profile" />
|
||||||
|
<AvatarFallback>
|
||||||
|
{userProfile.username[0]}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className={styles.profileDetails}>
|
||||||
|
<p>{userProfile?.username}</p>
|
||||||
|
{/* Connected Indicator */}
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<div className={`inline-flex h-4 w-4 rounded-full opacity-75 ${props.webSocketConnected ? 'bg-green-500' : 'bg-rose-500'}`}></div>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
{props.webSocketConnected ? "Connected" : "Disconnected"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.collapsed}`}>
|
<div className={`${styles.collapsed}`}>
|
||||||
{ userProfile &&
|
{userProfile &&
|
||||||
<div className={`${styles.profile}`}>
|
<div className={styles.profile}>
|
||||||
<img
|
<Avatar className="max-w-6 max-h-6 rounded-full">
|
||||||
className={styles.profile}
|
<AvatarImage src={userProfile.photo} alt="user profile" />
|
||||||
src={userProfile.photo}
|
<AvatarFallback>
|
||||||
alt="profile"
|
{userProfile.username[0]}
|
||||||
width={24}
|
</AvatarFallback>
|
||||||
height={24}
|
</Avatar>
|
||||||
/>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
|
||||||
<button className={styles.button} onClick={() => setEnabled(true)}>
|
<button className={styles.button} onClick={() => setEnabled(true)}>
|
||||||
{/* Pull Open Icon */}
|
{/* Pull Open Icon */}
|
||||||
<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="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>
|
<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>
|
||||||
@@ -146,6 +344,6 @@ export default function SidePanel() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,27 @@ div.session {
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
color: var(--main-text-color);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
max-width: 14rem;
|
max-width: 14rem;
|
||||||
|
font-size: medium;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(auto, 350px) 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.compressed {
|
||||||
|
grid-template-columns: minmax(auto, 12rem) 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sessionHover {
|
||||||
|
background-color: var(--secondary-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.session:hover {
|
||||||
|
background-color: var(--secondary-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.session a {
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.button {
|
button.button {
|
||||||
@@ -17,9 +35,6 @@ button.button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button.showMoreButton {
|
button.showMoreButton {
|
||||||
background: var(--intense-green);
|
|
||||||
border: none;
|
|
||||||
color: var(--frosted-background-color);
|
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
@@ -28,9 +43,8 @@ div.panel {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 1rem;
|
background-color: hsla(var(--primary-foreground));
|
||||||
background-color: var(--calm-blue);
|
background-color: var(--secondary-background-color);
|
||||||
color: var(--main-text-color);
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-width: auto;
|
max-width: auto;
|
||||||
@@ -47,14 +61,12 @@ div.collapsed {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.session:hover {
|
|
||||||
background-color: var(--calmer-blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
p.session {
|
p.session {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
text-align: left;
|
||||||
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.header {
|
div.header {
|
||||||
@@ -62,10 +74,18 @@ div.header {
|
|||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
img.profile {
|
div.profile {
|
||||||
width: 24px;
|
display: grid;
|
||||||
height: 24px;
|
grid-template-columns: auto 1fr;
|
||||||
border-radius: 50%;
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.panelWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -96,3 +116,13 @@ div.modalSessionsList div.session {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
div.panel {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.singleReference {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,14 @@
|
|||||||
--ring: 209.1 100% 40.8%;
|
--ring: 209.1 100% 40.8%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important;
|
--font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important;
|
||||||
|
|
||||||
|
/* Khoj Custom Colors */
|
||||||
--primary-hover: #fee285;
|
--primary-hover: #fee285;
|
||||||
|
--frosted-background-color: #F5F3F2;
|
||||||
|
--secondary-background-color: #F7F7F5;
|
||||||
|
--secondary-accent: #EDEDED;
|
||||||
|
--khoj-orange: #FFE7D1;
|
||||||
|
--border-color: #e2e2e2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
|||||||
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 }
|
||||||
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,
|
||||||
|
}
|
||||||
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 }
|
||||||
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 }
|
||||||
@@ -17,9 +17,15 @@
|
|||||||
"windowsexport": "yarn build && xcopy out ..\\..\\khoj\\interface\\built /E /Y && yarn windowscollectstatic"
|
"windowsexport": "yarn build && xcopy out ..\\..\\khoj\\interface\\built /E /Y && yarn windowscollectstatic"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@phosphor-icons/react": "^2.1.7",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-menubar": "^1.1.1",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@types/katex": "^0.16.7",
|
"@types/katex": "^0.16.7",
|
||||||
"@types/markdown-it": "^14.1.1",
|
"@types/markdown-it": "^14.1.1",
|
||||||
|
|||||||
@@ -318,6 +318,33 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f"
|
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f"
|
||||||
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
|
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
|
||||||
|
|
||||||
|
"@floating-ui/core@^1.0.0":
|
||||||
|
version "1.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.3.tgz#5e7bb92843f47fd1d8dcb9b3cc3c243aaed54f95"
|
||||||
|
integrity sha512-1ZpCvYf788/ZXOhRQGFxnYQOVgeU+pi0i+d0Ow34La7qjIXETi6RNswGVKkA6KcDO8/+Ysu2E/CeUmmeEBDvTg==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/utils" "^0.2.3"
|
||||||
|
|
||||||
|
"@floating-ui/dom@^1.0.0":
|
||||||
|
version "1.6.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.6.tgz#be54c1ab2d19112ad323e63dbeb08185fed0ffd3"
|
||||||
|
integrity sha512-qiTYajAnh3P+38kECeffMSQgbvXty2VB6rS+42iWR4FPIlZjLK84E9qtLnMTLIpPz2znD/TaFqaiavMUrS+Hcw==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/core" "^1.0.0"
|
||||||
|
"@floating-ui/utils" "^0.2.3"
|
||||||
|
|
||||||
|
"@floating-ui/react-dom@^2.0.0":
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.1.tgz#cca58b6b04fc92b4c39288252e285e0422291fb0"
|
||||||
|
integrity sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/dom" "^1.0.0"
|
||||||
|
|
||||||
|
"@floating-ui/utils@^0.2.3":
|
||||||
|
version "0.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.3.tgz#506fcc73f730affd093044cb2956c31ba6431545"
|
||||||
|
integrity sha512-XGndio0l5/Gvd6CLIABvsav9HHezgDFFhDfHk1bvLfr9ni8dojqLSvBbotJEjmIwNHL7vK4QzBJTdBRoB+c1ww==
|
||||||
|
|
||||||
"@humanwhocodes/config-array@^0.11.14":
|
"@humanwhocodes/config-array@^0.11.14":
|
||||||
version "0.11.14"
|
version "0.11.14"
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
|
||||||
@@ -459,11 +486,21 @@
|
|||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
|
"@phosphor-icons/react@^2.1.7":
|
||||||
|
version "2.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@phosphor-icons/react/-/react-2.1.7.tgz#b11a4b25849b7e3849970b688d9fe91e5d4fd8d7"
|
||||||
|
integrity sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==
|
||||||
|
|
||||||
"@pkgjs/parseargs@^0.11.0":
|
"@pkgjs/parseargs@^0.11.0":
|
||||||
version "0.11.0"
|
version "0.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||||
|
|
||||||
|
"@radix-ui/number@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.0.tgz#1e95610461a09cdf8bb05c152e76ca1278d5da46"
|
||||||
|
integrity sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==
|
||||||
|
|
||||||
"@radix-ui/primitive@1.1.0":
|
"@radix-ui/primitive@1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
|
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
|
||||||
@@ -481,6 +518,33 @@
|
|||||||
"@radix-ui/react-primitive" "2.0.0"
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
"@radix-ui/react-slot" "1.1.0"
|
"@radix-ui/react-slot" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-arrow@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz#744f388182d360b86285217e43b6c63633f39e7a"
|
||||||
|
integrity sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-avatar@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.0.tgz#457c81334c93f4608df15f081e7baa286558d6a2"
|
||||||
|
integrity sha512-Q/PbuSMk/vyAd/UoIShVGZ7StHHeRFYU7wXmi5GV+8cLXflZAEpHL/F697H1klrzxKXNtZ97vWiC0q3RKUH8UA==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-context" "1.1.0"
|
||||||
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-collection@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed"
|
||||||
|
integrity sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.0"
|
||||||
|
"@radix-ui/react-context" "1.1.0"
|
||||||
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
"@radix-ui/react-slot" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs@1.1.0":
|
"@radix-ui/react-compose-refs@1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74"
|
||||||
@@ -511,6 +575,11 @@
|
|||||||
aria-hidden "^1.1.1"
|
aria-hidden "^1.1.1"
|
||||||
react-remove-scroll "2.5.7"
|
react-remove-scroll "2.5.7"
|
||||||
|
|
||||||
|
"@radix-ui/react-direction@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc"
|
||||||
|
integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==
|
||||||
|
|
||||||
"@radix-ui/react-dismissable-layer@1.1.0":
|
"@radix-ui/react-dismissable-layer@1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz#2cd0a49a732372513733754e6032d3fb7988834e"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz#2cd0a49a732372513733754e6032d3fb7988834e"
|
||||||
@@ -522,6 +591,19 @@
|
|||||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
"@radix-ui/react-use-escape-keydown" "1.1.0"
|
"@radix-ui/react-use-escape-keydown" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-dropdown-menu@^2.1.1":
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.1.tgz#3dc578488688250dbbe109d9ff2ca28a9bca27ec"
|
||||||
|
integrity sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.0"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.0"
|
||||||
|
"@radix-ui/react-context" "1.1.0"
|
||||||
|
"@radix-ui/react-id" "1.1.0"
|
||||||
|
"@radix-ui/react-menu" "2.1.1"
|
||||||
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-focus-guards@1.1.0":
|
"@radix-ui/react-focus-guards@1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13"
|
||||||
@@ -550,6 +632,82 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-primitive" "2.0.0"
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-menu@2.1.1":
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.1.tgz#bd623ace0e1ae1ac78023a505fec0541d59fb346"
|
||||||
|
integrity sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.0"
|
||||||
|
"@radix-ui/react-collection" "1.1.0"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.0"
|
||||||
|
"@radix-ui/react-context" "1.1.0"
|
||||||
|
"@radix-ui/react-direction" "1.1.0"
|
||||||
|
"@radix-ui/react-dismissable-layer" "1.1.0"
|
||||||
|
"@radix-ui/react-focus-guards" "1.1.0"
|
||||||
|
"@radix-ui/react-focus-scope" "1.1.0"
|
||||||
|
"@radix-ui/react-id" "1.1.0"
|
||||||
|
"@radix-ui/react-popper" "1.2.0"
|
||||||
|
"@radix-ui/react-portal" "1.1.1"
|
||||||
|
"@radix-ui/react-presence" "1.1.0"
|
||||||
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
"@radix-ui/react-roving-focus" "1.1.0"
|
||||||
|
"@radix-ui/react-slot" "1.1.0"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
aria-hidden "^1.1.1"
|
||||||
|
react-remove-scroll "2.5.7"
|
||||||
|
|
||||||
|
"@radix-ui/react-menubar@^1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-menubar/-/react-menubar-1.1.1.tgz#e126514cb1c46e0a4f9fba7d016e578cc4e41f22"
|
||||||
|
integrity sha512-V05Hryq/BE2m+rs8d5eLfrS0jmSWSDHEbG7jEyLA5D5J9jTvWj/o3v3xDN9YsOlH6QIkJgiaNDaP+S4T1rdykw==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.0"
|
||||||
|
"@radix-ui/react-collection" "1.1.0"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.0"
|
||||||
|
"@radix-ui/react-context" "1.1.0"
|
||||||
|
"@radix-ui/react-direction" "1.1.0"
|
||||||
|
"@radix-ui/react-id" "1.1.0"
|
||||||
|
"@radix-ui/react-menu" "2.1.1"
|
||||||
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
"@radix-ui/react-roving-focus" "1.1.0"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-navigation-menu@^1.2.0":
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.0.tgz#884c9b9fd141cc5db257bd3f6bf3b84e349c6617"
|
||||||
|
integrity sha512-OQ8tcwAOR0DhPlSY3e4VMXeHiol7la4PPdJWhhwJiJA+NLX0SaCaonOkRnI3gCDHoZ7Fo7bb/G6q25fRM2Y+3Q==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.0"
|
||||||
|
"@radix-ui/react-collection" "1.1.0"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.0"
|
||||||
|
"@radix-ui/react-context" "1.1.0"
|
||||||
|
"@radix-ui/react-direction" "1.1.0"
|
||||||
|
"@radix-ui/react-dismissable-layer" "1.1.0"
|
||||||
|
"@radix-ui/react-id" "1.1.0"
|
||||||
|
"@radix-ui/react-presence" "1.1.0"
|
||||||
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
"@radix-ui/react-use-previous" "1.1.0"
|
||||||
|
"@radix-ui/react-visually-hidden" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-popper@1.2.0":
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz#a3e500193d144fe2d8f5d5e60e393d64111f2a7a"
|
||||||
|
integrity sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/react-dom" "^2.0.0"
|
||||||
|
"@radix-ui/react-arrow" "1.1.0"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.0"
|
||||||
|
"@radix-ui/react-context" "1.1.0"
|
||||||
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
"@radix-ui/react-use-rect" "1.1.0"
|
||||||
|
"@radix-ui/react-use-size" "1.1.0"
|
||||||
|
"@radix-ui/rect" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-portal@1.1.1":
|
"@radix-ui/react-portal@1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.1.tgz#1957f1eb2e1aedfb4a5475bd6867d67b50b1d15f"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.1.tgz#1957f1eb2e1aedfb4a5475bd6867d67b50b1d15f"
|
||||||
@@ -573,6 +731,36 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-slot" "1.1.0"
|
"@radix-ui/react-slot" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-roving-focus@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e"
|
||||||
|
integrity sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.0"
|
||||||
|
"@radix-ui/react-collection" "1.1.0"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.0"
|
||||||
|
"@radix-ui/react-context" "1.1.0"
|
||||||
|
"@radix-ui/react-direction" "1.1.0"
|
||||||
|
"@radix-ui/react-id" "1.1.0"
|
||||||
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-scroll-area@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz#50b24b0fc9ada151d176395bcf47b2ec68feada5"
|
||||||
|
integrity sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/number" "1.1.0"
|
||||||
|
"@radix-ui/primitive" "1.1.0"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.0"
|
||||||
|
"@radix-ui/react-context" "1.1.0"
|
||||||
|
"@radix-ui/react-direction" "1.1.0"
|
||||||
|
"@radix-ui/react-presence" "1.1.0"
|
||||||
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.1.0":
|
"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84"
|
||||||
@@ -604,6 +792,37 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
|
||||||
integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==
|
integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==
|
||||||
|
|
||||||
|
"@radix-ui/react-use-previous@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz#d4dd37b05520f1d996a384eb469320c2ada8377c"
|
||||||
|
integrity sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==
|
||||||
|
|
||||||
|
"@radix-ui/react-use-rect@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88"
|
||||||
|
integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/rect" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-use-size@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b"
|
||||||
|
integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-visually-hidden@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz#ad47a8572580f7034b3807c8e6740cd41038a5a2"
|
||||||
|
integrity sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
|
||||||
|
"@radix-ui/rect@1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438"
|
||||||
|
integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==
|
||||||
|
|
||||||
"@rushstack/eslint-patch@^1.3.3":
|
"@rushstack/eslint-patch@^1.3.3":
|
||||||
version "1.10.3"
|
version "1.10.3"
|
||||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz#391d528054f758f81e53210f1a1eebcf1a8b1d20"
|
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz#391d528054f758f81e53210f1a1eebcf1a8b1d20"
|
||||||
|
|||||||
@@ -644,7 +644,11 @@ class ConversationAdapters:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_conversation_sessions(user: KhojUser, client_application: ClientApplication = None):
|
def get_conversation_sessions(user: KhojUser, client_application: ClientApplication = None):
|
||||||
return Conversation.objects.filter(user=user, client=client_application).order_by("-updated_at")
|
return (
|
||||||
|
Conversation.objects.filter(user=user, client=client_application)
|
||||||
|
.prefetch_related("agent")
|
||||||
|
.order_by("-updated_at")
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def aset_conversation_title(
|
async def aset_conversation_title(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Dict, Optional
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
@@ -15,7 +15,6 @@ from websockets import ConnectionClosedOK
|
|||||||
|
|
||||||
from khoj.database.adapters import (
|
from khoj.database.adapters import (
|
||||||
ConversationAdapters,
|
ConversationAdapters,
|
||||||
DataStoreAdapters,
|
|
||||||
EntryAdapters,
|
EntryAdapters,
|
||||||
FileObjectAdapters,
|
FileObjectAdapters,
|
||||||
PublicConversationAdapters,
|
PublicConversationAdapters,
|
||||||
@@ -95,53 +94,6 @@ def get_file_filter(request: Request, conversation_id: str) -> Response:
|
|||||||
return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
|
return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
|
||||||
|
|
||||||
|
|
||||||
class FactCheckerStoreDataFormat(BaseModel):
|
|
||||||
factToVerify: str
|
|
||||||
response: str
|
|
||||||
references: Any
|
|
||||||
childReferences: List[Any]
|
|
||||||
runId: str
|
|
||||||
modelUsed: Dict[str, Any]
|
|
||||||
|
|
||||||
|
|
||||||
class FactCheckerStoreData(BaseModel):
|
|
||||||
runId: str
|
|
||||||
storeData: FactCheckerStoreDataFormat
|
|
||||||
|
|
||||||
|
|
||||||
@api_chat.post("/store/factchecker", response_class=Response)
|
|
||||||
@requires(["authenticated"])
|
|
||||||
async def store_factchecker(request: Request, common: CommonQueryParams, data: FactCheckerStoreData):
|
|
||||||
user = request.user.object
|
|
||||||
|
|
||||||
update_telemetry_state(
|
|
||||||
request=request,
|
|
||||||
telemetry_type="api",
|
|
||||||
api="store_factchecker",
|
|
||||||
**common.__dict__,
|
|
||||||
)
|
|
||||||
fact_checker_key = f"factchecker_{data.runId}"
|
|
||||||
await DataStoreAdapters.astore_data(data.storeData.model_dump_json(), fact_checker_key, user, private=False)
|
|
||||||
return Response(content=json.dumps({"status": "ok"}), media_type="application/json", status_code=200)
|
|
||||||
|
|
||||||
|
|
||||||
@api_chat.get("/store/factchecker", response_class=Response)
|
|
||||||
async def get_factchecker(request: Request, common: CommonQueryParams, runId: str):
|
|
||||||
update_telemetry_state(
|
|
||||||
request=request,
|
|
||||||
telemetry_type="api",
|
|
||||||
api="read_factchecker",
|
|
||||||
**common.__dict__,
|
|
||||||
)
|
|
||||||
|
|
||||||
fact_checker_key = f"factchecker_{runId}"
|
|
||||||
|
|
||||||
data = await DataStoreAdapters.aretrieve_public_data(fact_checker_key)
|
|
||||||
if data is None:
|
|
||||||
return Response(status_code=404)
|
|
||||||
return Response(content=json.dumps(data.value), media_type="application/json", status_code=200)
|
|
||||||
|
|
||||||
|
|
||||||
@api_chat.post("/conversation/file-filters", response_class=Response)
|
@api_chat.post("/conversation/file-filters", response_class=Response)
|
||||||
@requires(["authenticated"])
|
@requires(["authenticated"])
|
||||||
def add_file_filter(request: Request, filter: FilterRequest):
|
def add_file_filter(request: Request, filter: FilterRequest):
|
||||||
@@ -418,15 +370,26 @@ def duplicate_chat_history_public_conversation(
|
|||||||
def chat_sessions(
|
def chat_sessions(
|
||||||
request: Request,
|
request: Request,
|
||||||
common: CommonQueryParams,
|
common: CommonQueryParams,
|
||||||
|
recent: Optional[bool] = False,
|
||||||
):
|
):
|
||||||
user = request.user.object
|
user = request.user.object
|
||||||
|
|
||||||
# Load Conversation Sessions
|
# Load Conversation Sessions
|
||||||
sessions = ConversationAdapters.get_conversation_sessions(user, request.user.client_app).values_list(
|
conversations = ConversationAdapters.get_conversation_sessions(user, request.user.client_app)
|
||||||
"id", "slug", "title"
|
if recent:
|
||||||
)
|
conversations = conversations[:8]
|
||||||
|
|
||||||
session_values = [{"conversation_id": session[0], "slug": session[2] or session[1]} for session in sessions]
|
sessions = conversations.values_list("id", "slug", "title", "agent__slug", "agent__name", "agent__avatar")
|
||||||
|
|
||||||
|
session_values = [
|
||||||
|
{
|
||||||
|
"conversation_id": session[0],
|
||||||
|
"slug": session[2] or session[1],
|
||||||
|
"agent_name": session[4],
|
||||||
|
"agent_avatar": session[5],
|
||||||
|
}
|
||||||
|
for session in sessions
|
||||||
|
]
|
||||||
|
|
||||||
update_telemetry_state(
|
update_telemetry_state(
|
||||||
request=request,
|
request=request,
|
||||||
|
|||||||
Reference in New Issue
Block a user