mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-09 05:39:12 +00:00
Finish up filte filter side panel menu
This commit is contained in:
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)));
|
||||||
}
|
}
|
||||||
}, [searchInput, files]);
|
|
||||||
|
setFilteredFiles(sortedFiles);
|
||||||
|
}
|
||||||
|
}, [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) ?
|
||||||
|
<Button
|
||||||
|
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}
|
{filename}
|
||||||
</div>
|
<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>
|
||||||
@@ -560,12 +560,14 @@ function UserProfileComponent(props: UserProfileProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.profile}>
|
<div className={styles.profile}>
|
||||||
|
<Link href="/config" target="_blank" rel="noopener noreferrer">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={props.userProfile.photo} alt="user profile" />
|
<AvatarImage src={props.userProfile.photo} alt="user profile" />
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{props.userProfile.username[0]}
|
{props.userProfile.username[0]}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
</Link>
|
||||||
<div className={styles.profileDetails}>
|
<div className={styles.profileDetails}>
|
||||||
<p>{props.userProfile?.username}</p>
|
<p>{props.userProfile?.username}</p>
|
||||||
{/* Connected Indicator */}
|
{/* Connected Indicator */}
|
||||||
@@ -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>
|
||||||
:
|
:
|
||||||
|
|||||||
Reference in New Issue
Block a user