Finish up filte filter side panel menu

This commit is contained in:
sabaimran
2024-07-02 23:32:36 +05:30
parent 8a6722ba97
commit 78d1a29bc1
3 changed files with 110 additions and 107 deletions

View File

@@ -144,7 +144,7 @@ export default function Chat() {
Khoj AI - {title} Khoj AI - {title}
</title> </title>
<div className={styles.sidePanel}> <div className={styles.sidePanel}>
<SidePanel webSocketConnected={chatWS !== null} /> <SidePanel webSocketConnected={chatWS !== null} conversationId={conversationId} />
</div> </div>
<div className={styles.chatBox}> <div className={styles.chatBox}>
<NavMenu selected="Chat" title={title} /> <NavMenu selected="Chat" title={title} />

View File

@@ -80,17 +80,17 @@ export default function NavMenu(props: NavMenuProps) {
: :
<Menubar className='items-top inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground'> <Menubar className='items-top inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground'>
<MenubarMenu> <MenubarMenu>
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}> <Link href='/chat' target="_blank" rel="noreferrer" className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>
<MenubarTrigger>Chat</MenubarTrigger> <MenubarTrigger>Chat</MenubarTrigger>
</Link> </Link>
</MenubarMenu> </MenubarMenu>
<MenubarMenu> <MenubarMenu>
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}> <Link href='/agents' target="_blank" rel="noreferrer" className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>
<MenubarTrigger>Agents</MenubarTrigger> <MenubarTrigger>Agents</MenubarTrigger>
</Link> </Link>
</MenubarMenu> </MenubarMenu>
<MenubarMenu> <MenubarMenu>
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}> <Link href='/automations' target="_blank" rel="noreferrer" className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>
<MenubarTrigger>Automations</MenubarTrigger> <MenubarTrigger>Automations</MenubarTrigger>
</Link> </Link>
</MenubarMenu> </MenubarMenu>

View File

@@ -2,7 +2,7 @@
import styles from "./sidePanel.module.css"; import styles from "./sidePanel.module.css";
import { useEffect, useState } from "react"; import { Suspense, useEffect, useState } from "react";
import { UserProfile } from "@/app/common/auth"; import { UserProfile } from "@/app/common/auth";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
@@ -29,7 +29,7 @@ import {
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { ArrowRight, ArrowLeft, ArrowDown, Spinner } from "@phosphor-icons/react"; import { ArrowRight, ArrowLeft, ArrowDown, Spinner, Check } from "@phosphor-icons/react";
interface ChatHistory { interface ChatHistory {
conversation_id: string; conversation_id: string;
@@ -124,60 +124,91 @@ function deleteConversation(conversationId: string) {
}); });
} }
function modifyFileFilterForConversation(
conversationId: string | null,
filename: string,
setAddedFiles: (files: string[]) => void,
mode: 'add' | 'remove') {
if (!conversationId) {
console.error("No conversation ID provided");
return;
}
const method = mode === 'add' ? 'POST' : 'DELETE';
const body = {
conversation_id: conversationId,
filename: filename,
}
const addUrl = `/api/chat/conversation/file-filters`;
fetch(addUrl, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
.then(response => response.json())
.then(data => {
setAddedFiles(data);
console.log(data);
})
.catch(err => {
console.error(err);
return;
});
}
interface FilesMenuProps { interface FilesMenuProps {
conversationId: string; conversationId: string | null;
} }
function FilesMenu(props: FilesMenuProps) { function FilesMenu(props: FilesMenuProps) {
// Use SWR to fetch files // Use SWR to fetch files
const { data: files, error } = useSWR('/api/config/data/computer', fetcher); const { data: files, error } = useSWR(props.conversationId ? '/api/config/data/computer' : null, fetcher);
const { data: selectedFiles, error: selectedFilesError } = useSWR(`/api/chat/conversation/file-filters/${props.conversationId}`, fetcher); const { data: selectedFiles, error: selectedFilesError } = useSWR(props.conversationId ? `/api/chat/conversation/file-filters/${props.conversationId}` : null, fetcher);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [searchInput, setSearchInput] = useState(''); const [searchInput, setSearchInput] = useState('');
const [filteredFiles, setFilteredFiles] = useState<string[]>([]); const [filteredFiles, setFilteredFiles] = useState<string[]>([]);
const [addedFiles, setAddedFiles] = useState<string[]>([]);
// Function to handle file click
const handleFileClick = (filename: string) => {
console.log(`File clicked: ${filename}`);
// Implement the logic you want to execute on file click
};
useEffect(() => { useEffect(() => {
if (!files) return; if (!files) return;
if (searchInput === '') { if (searchInput === '') {
setFilteredFiles(files); setFilteredFiles(files);
} else { } else {
let filteredFiles = files.filter((filename: string) => filename.toLowerCase().includes(searchInput.toLowerCase())); let sortedFiles = files.filter((filename: string) => filename.toLowerCase().includes(searchInput.toLowerCase()));
setFilteredFiles(filteredFiles);
if (addedFiles) {
sortedFiles = addedFiles.concat(filteredFiles.filter((filename: string) => !addedFiles.includes(filename)));
}
setFilteredFiles(sortedFiles);
} }
}, [searchInput, files]); }, [searchInput, files, addedFiles]);
useEffect(() => {
if (selectedFiles) {
setAddedFiles(selectedFiles);
}
}, [selectedFiles]);
if (!props.conversationId) return (<></>);
if (error) return <div>Failed to load files</div>; if (error) return <div>Failed to load files</div>;
if (selectedFilesError) return <div>Failed to load selected files</div>;
if (!files) return <InlineLoading />; if (!files) return <InlineLoading />;
if (!selectedFiles) return <InlineLoading />;
return ( return (
<> <>
{/* <ScrollArea className="h-[40vh] w-[14rem]">
<ul className="indexed-files">
{files.length === 0 ? (
<div className="no-files-message">
<a className="inline-chat-link" href="https://docs.khoj.dev/category/clients/">How to upload files</a>
</div>
) : (
files.map((filename: string) => (
<li key={filename} className="fileName" id={filename} onClick={() => handleFileClick(filename)}>
{filename}
</li>
))
)}
</ul>
{files.length > 0 && <button className="file-toggle-button" style={{ display: "block" }}>Toggle Files</button>}
</ScrollArea> */}
<Popover <Popover
open={isOpen} open={isOpen}
onOpenChange={setIsOpen}> onOpenChange={setIsOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<div <div
className="w-auto bg-background border border-muted p-4 drop-shadow-sm rounded-2xl"> className="w-auto bg-background border border-muted p-4 drop-shadow-sm rounded-2xl">
@@ -185,7 +216,7 @@ function FilesMenu(props: FilesMenuProps) {
<h4 className="text-sm font-semibold"> <h4 className="text-sm font-semibold">
Manage Files Manage Files
<p> <p>
<span className="text-muted-foreground text-xs">Using {files.length} files</span> <span className="text-muted-foreground text-xs">Using {addedFiles.length == 0 ? files.length : addedFiles.length} files</span>
</p> </p>
</h4> </h4>
<Button variant="ghost" size="sm" className="w-9 p-0"> <Button variant="ghost" size="sm" className="w-9 p-0">
@@ -216,60 +247,27 @@ function FilesMenu(props: FilesMenuProps) {
} }
{ {
filteredFiles.map((filename: string) => ( filteredFiles.map((filename: string) => (
<div key={filename} className="rounded-md border-none py-2 text-sm text-wrap break-words"> addedFiles && addedFiles.includes(filename) ?
{filename} <Button
</div> variant={'ghost'}
key={filename}
className="rounded-md border-none py-2 text-sm text-wrap break-words bg-accent text-accent-foreground text-left"
onClick={() => modifyFileFilterForConversation(props.conversationId, filename, setAddedFiles, 'remove')}>
{filename}
<Check className="h-4 w-4 ml-2" />
</Button>
:
<Button
variant={'ghost'}
key={filename}
className="rounded-md border-none py-2 text-sm text-wrap break-words text-left"
onClick={() => modifyFileFilterForConversation(props.conversationId, filename, setAddedFiles, 'add')}>
{filename}
</Button>
)) ))
} }
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{/* <Collapsible
open={isOpen}
onOpenChange={setIsOpen}
className="w-auto bg-background border border-muted p-4 drop-shadow-sm rounded-2xl"
>
<div className="flex items-center justify-between space-x-4">
<h4 className="text-sm font-semibold">
Manage Files
<p>
<span className="text-muted-foreground text-xs">Using {files.length} files</span>
</p>
</h4>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-9 p-0">
{
isOpen ?
<ArrowDown className="h-4 w-4" />
:
<ArrowRight className="h-4 w-4" />
}
<span className="sr-only">Toggle</span>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="space-y-2 w-min min-w-[14rem]">
<Input
placeholder="Find file"
className="rounded-md border-none py-2 text-sm text-wrap break-words my-2 bg-accent text-accent-foreground"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)} />
{
filteredFiles.length === 0 && (
<div className="rounded-md border-none py-2 text-sm text-wrap break-words">
No files found
</div>
)
}
{
filteredFiles.map((filename: string) => (
<div key={filename} className="rounded-md border-none py-2 text-sm text-wrap break-words">
{filename}
</div>
))
}
</CollapsibleContent>
</Collapsible> */}
</> </>
) )
@@ -282,6 +280,7 @@ interface SessionsAndFilesProps {
organizedData: GroupedChatHistory | null; organizedData: GroupedChatHistory | null;
data: ChatHistory[] | null; data: ChatHistory[] | null;
userProfile: UserProfile | null; userProfile: UserProfile | null;
conversationId: string | null;
} }
function SessionsAndFiles(props: SessionsAndFilesProps) { function SessionsAndFiles(props: SessionsAndFilesProps) {
@@ -296,13 +295,13 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
<div className={styles.sessionsList}> <div className={styles.sessionsList}>
{props.subsetOrganizedData != null && Object.keys(props.subsetOrganizedData).map((agentName) => ( {props.subsetOrganizedData != null && Object.keys(props.subsetOrganizedData).map((agentName) => (
<div key={agentName} className={`my-4`}> <div key={agentName} className={`my-4`}>
<h3 className={`grid grid-flow-col auto-cols-max gap-2 my-4 font-bold text-sm`}> {/* <h3 className={`grid grid-flow-col auto-cols-max gap-2 my-4 font-bold text-sm`}>
{ {
props.subsetOrganizedData && props.subsetOrganizedData &&
<img src={props.subsetOrganizedData[agentName][0].agent_avatar} alt={agentName} width={24} height={24} /> <img src={props.subsetOrganizedData[agentName][0].agent_avatar} alt={agentName} width={24} height={24} />
} }
{agentName} {agentName}
</h3> </h3> */}
{props.subsetOrganizedData && props.subsetOrganizedData[agentName].map((chatHistory) => ( {props.subsetOrganizedData && props.subsetOrganizedData[agentName].map((chatHistory) => (
<ChatSession <ChatSession
compressed={true} compressed={true}
@@ -321,7 +320,7 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
<ChatSessionsModal data={props.organizedData} /> <ChatSessionsModal data={props.organizedData} />
) )
} }
<FilesMenu /> <FilesMenu conversationId={props.conversationId} />
{props.userProfile && {props.userProfile &&
<UserProfileComponent userProfile={props.userProfile} webSocketConnected={props.webSocketConnected} collapsed={false} /> <UserProfileComponent userProfile={props.userProfile} webSocketConnected={props.webSocketConnected} collapsed={false} />
}</> }</>
@@ -370,6 +369,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
<Button <Button
onClick={() => { onClick={() => {
renameConversation(props.conversationId, renamedTitle); renameConversation(props.conversationId, renamedTitle);
setIsRenaming(false);
}} }}
type="submit">Rename</Button> type="submit">Rename</Button>
</DialogFooter> </DialogFooter>
@@ -385,7 +385,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
return ( return (
<Dialog <Dialog
open={isSharing || showShareUrl} open={isSharing || showShareUrl}
onOpenChange={(open) => { onOpenChange={(open) => {
setShowShareUrl(open) setShowShareUrl(open)
setIsSharing(open) setIsSharing(open)
}}> }}>
@@ -559,24 +559,26 @@ function UserProfileComponent(props: UserProfileProps) {
} }
return ( return (
<div className={styles.profile}> <div className={styles.profile}>
<Avatar> <Link href="/config" target="_blank" rel="noopener noreferrer">
<AvatarImage src={props.userProfile.photo} alt="user profile" /> <Avatar>
<AvatarFallback> <AvatarImage src={props.userProfile.photo} alt="user profile" />
{props.userProfile.username[0]} <AvatarFallback>
</AvatarFallback> {props.userProfile.username[0]}
</Avatar> </AvatarFallback>
<div className={styles.profileDetails}> </Avatar>
<p>{props.userProfile?.username}</p> </Link>
{/* Connected Indicator */} <div className={styles.profileDetails}>
<div className="flex gap-2 items-center"> <p>{props.userProfile?.username}</p>
<div className={`inline-flex h-4 w-4 rounded-full opacity-75 ${props.webSocketConnected ? 'bg-green-500' : 'bg-rose-500'}`}></div> {/* Connected Indicator */}
<p className="text-muted-foreground text-sm"> <div className="flex gap-2 items-center">
{props.webSocketConnected ? "Connected" : "Disconnected"} <div className={`inline-flex h-4 w-4 rounded-full opacity-75 ${props.webSocketConnected ? 'bg-green-500' : 'bg-rose-500'}`}></div>
</p> <p className="text-muted-foreground text-sm">
{props.webSocketConnected ? "Connected" : "Disconnected"}
</p>
</div>
</div> </div>
</div> </div>
</div>
); );
} }
@@ -603,6 +605,7 @@ export const useChatHistoryRecentFetchRequest = (url: string) => {
interface SidePanelProps { interface SidePanelProps {
webSocketConnected?: boolean; webSocketConnected?: boolean;
conversationId: string | null;
} }
@@ -611,7 +614,6 @@ export default function SidePanel(props: SidePanelProps) {
const [data, setData] = useState<ChatHistory[] | null>(null); const [data, setData] = useState<ChatHistory[] | null>(null);
const [organizedData, setOrganizedData] = useState<GroupedChatHistory | null>(null); const [organizedData, setOrganizedData] = useState<GroupedChatHistory | null>(null);
const [subsetOrganizedData, setSubsetOrganizedData] = useState<GroupedChatHistory | null>(null); const [subsetOrganizedData, setSubsetOrganizedData] = useState<GroupedChatHistory | null>(null);
const [isLoading, setLoading] = useState(true)
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
const [userProfile, setUserProfile] = useState<UserProfile | null>(null); const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
@@ -670,6 +672,7 @@ export default function SidePanel(props: SidePanelProps) {
organizedData={organizedData} organizedData={organizedData}
data={data} data={data}
userProfile={userProfile} userProfile={userProfile}
conversationId={props.conversationId}
/> />
</div> </div>
: :