mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-09-15 14:01:31 +00:00
Compare commits
5 Commits
7ce00f4605
...
feat/model
Author | SHA1 | Date | |
---|---|---|---|
|
701819d018 | ||
|
68e151b2bd | ||
|
06ff272541 | ||
|
4154d5e4b1 | ||
|
8aaee2c40c |
@@ -1,12 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import DeleteChat from '@/components/DeleteChat';
|
import DeleteChat from '@/components/DeleteChat';
|
||||||
import BatchDeleteChats from '@/components/BatchDeleteChats';
|
|
||||||
import { cn, formatTimeDifference } from '@/lib/utils';
|
import { cn, formatTimeDifference } from '@/lib/utils';
|
||||||
import { BookOpenText, Check, ClockIcon, Delete, ScanEye, Search, X } from 'lucide-react';
|
import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export interface Chat {
|
export interface Chat {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -17,13 +15,7 @@ export interface Chat {
|
|||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const [chats, setChats] = useState<Chat[]>([]);
|
const [chats, setChats] = useState<Chat[]>([]);
|
||||||
const [filteredChats, setFilteredChats] = useState<Chat[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [selectionMode, setSelectionMode] = useState(false);
|
|
||||||
const [selectedChats, setSelectedChats] = useState<string[]>([]);
|
|
||||||
const [hoveredChatId, setHoveredChatId] = useState<string | null>(null);
|
|
||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchChats = async () => {
|
const fetchChats = async () => {
|
||||||
@@ -39,71 +31,12 @@ const Page = () => {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
setChats(data.chats);
|
setChats(data.chats);
|
||||||
setFilteredChats(data.chats);
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchChats();
|
fetchChats();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (searchQuery.trim() === '') {
|
|
||||||
setFilteredChats(chats);
|
|
||||||
} else {
|
|
||||||
const filtered = chats.filter((chat) =>
|
|
||||||
chat.title.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
);
|
|
||||||
setFilteredChats(filtered);
|
|
||||||
}
|
|
||||||
}, [searchQuery, chats]);
|
|
||||||
|
|
||||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setSearchQuery(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearSearch = () => {
|
|
||||||
setSearchQuery('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleSelectionMode = () => {
|
|
||||||
setSelectionMode(!selectionMode);
|
|
||||||
setSelectedChats([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleChatSelection = (chatId: string) => {
|
|
||||||
if (selectedChats.includes(chatId)) {
|
|
||||||
setSelectedChats(selectedChats.filter(id => id !== chatId));
|
|
||||||
} else {
|
|
||||||
setSelectedChats([...selectedChats, chatId]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectAllChats = () => {
|
|
||||||
if (selectedChats.length === filteredChats.length) {
|
|
||||||
setSelectedChats([]);
|
|
||||||
} else {
|
|
||||||
setSelectedChats(filteredChats.map(chat => chat.id));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteSelectedChats = () => {
|
|
||||||
if (selectedChats.length === 0) return;
|
|
||||||
setIsDeleteDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBatchDeleteComplete = () => {
|
|
||||||
setSelectedChats([]);
|
|
||||||
setSelectionMode(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateChatsAfterDelete = (newChats: Chat[]) => {
|
|
||||||
setChats(newChats);
|
|
||||||
setFilteredChats(newChats.filter(chat =>
|
|
||||||
searchQuery.trim() === '' ||
|
|
||||||
chat.title.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
return loading ? (
|
return loading ? (
|
||||||
<div className="flex flex-row items-center justify-center min-h-screen">
|
<div className="flex flex-row items-center justify-center min-h-screen">
|
||||||
<svg
|
<svg
|
||||||
@@ -131,145 +64,32 @@ const Page = () => {
|
|||||||
<h1 className="text-3xl font-medium p-2">Library</h1>
|
<h1 className="text-3xl font-medium p-2">Library</h1>
|
||||||
</div>
|
</div>
|
||||||
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
||||||
|
|
||||||
{/* Search Box */}
|
|
||||||
<div className="relative mt-6 mb-6">
|
|
||||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
|
||||||
<Search className="w-5 h-5 text-black/50 dark:text-white/50" />
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
{chats.length === 0 && (
|
||||||
type="text"
|
<div className="flex flex-row items-center justify-center min-h-screen">
|
||||||
className="block w-full p-2 pl-10 pr-10 bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 rounded-md text-black dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
||||||
placeholder="Search your threads..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
/>
|
|
||||||
{searchQuery && (
|
|
||||||
<button
|
|
||||||
onClick={clearSearch}
|
|
||||||
className="absolute inset-y-0 right-0 flex items-center pr-3"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5 text-black/50 dark:text-white/50 hover:text-black dark:hover:text-white" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Thread Count and Selection Controls */}
|
|
||||||
<div className="mb-4">
|
|
||||||
{!selectionMode ? (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="text-black/70 dark:text-white/70">
|
|
||||||
You have {chats.length} threads in Perplexica
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={toggleSelectionMode}
|
|
||||||
className="text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white text-sm transition duration-200"
|
|
||||||
>
|
|
||||||
Select
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="text-black/70 dark:text-white/70">
|
|
||||||
{selectedChats.length} selected thread{selectedChats.length !== 1 ? 's' : ''}
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-4">
|
|
||||||
<button
|
|
||||||
onClick={selectAllChats}
|
|
||||||
className="text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white text-sm transition duration-200"
|
|
||||||
>
|
|
||||||
{selectedChats.length === filteredChats.length ? 'Deselect all' : 'Select all'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={toggleSelectionMode}
|
|
||||||
className="text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white text-sm transition duration-200"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={deleteSelectedChats}
|
|
||||||
disabled={selectedChats.length === 0}
|
|
||||||
className={cn(
|
|
||||||
"text-sm transition duration-200",
|
|
||||||
selectedChats.length === 0
|
|
||||||
? "text-red-400/50 hover:text-red-500/50 cursor-not-allowed"
|
|
||||||
: "text-red-400 hover:text-red-500 cursor-pointer"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Delete Selected
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filteredChats.length === 0 && (
|
|
||||||
<div className="flex flex-row items-center justify-center min-h-[50vh]">
|
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
{searchQuery ? 'No threads found matching your search.' : 'No threads found.'}
|
No chats found.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{chats.length > 0 && (
|
||||||
{filteredChats.length > 0 && (
|
|
||||||
<div className="flex flex-col pb-20 lg:pb-2">
|
<div className="flex flex-col pb-20 lg:pb-2">
|
||||||
{filteredChats.map((chat, i) => (
|
{chats.map((chat, i) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col space-y-4 py-6',
|
'flex flex-col space-y-4 py-6',
|
||||||
i !== filteredChats.length - 1
|
i !== chats.length - 1
|
||||||
? 'border-b border-white-200 dark:border-dark-200'
|
? 'border-b border-white-200 dark:border-dark-200'
|
||||||
: '',
|
: '',
|
||||||
)}
|
)}
|
||||||
key={i}
|
key={i}
|
||||||
onMouseEnter={() => setHoveredChatId(chat.id)}
|
|
||||||
onMouseLeave={() => setHoveredChatId(null)}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
|
||||||
{/* Checkbox - visible when in selection mode or when hovering */}
|
|
||||||
{(selectionMode || hoveredChatId === chat.id) && (
|
|
||||||
<div
|
|
||||||
className="mr-3 cursor-pointer"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!selectionMode) setSelectionMode(true);
|
|
||||||
toggleChatSelection(chat.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={cn(
|
|
||||||
"w-5 h-5 border rounded flex items-center justify-center transition-colors",
|
|
||||||
selectedChats.includes(chat.id)
|
|
||||||
? "bg-blue-500 border-blue-500"
|
|
||||||
: "border-gray-400 dark:border-gray-600"
|
|
||||||
)}>
|
|
||||||
{selectedChats.includes(chat.id) && (
|
|
||||||
<Check className="w-4 h-4 text-white" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Chat Title */}
|
|
||||||
<Link
|
<Link
|
||||||
href={`/c/${chat.id}`}
|
href={`/c/${chat.id}`}
|
||||||
className={cn(
|
className="text-black dark:text-white lg:text-xl font-medium truncate transition duration-200 hover:text-[#24A0ED] dark:hover:text-[#24A0ED] cursor-pointer"
|
||||||
"text-black dark:text-white lg:text-xl font-medium truncate transition duration-200 hover:text-[#24A0ED] dark:hover:text-[#24A0ED] cursor-pointer",
|
|
||||||
selectionMode && "pointer-events-none text-black dark:text-white hover:text-black dark:hover:text-white"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (selectionMode) {
|
|
||||||
e.preventDefault();
|
|
||||||
toggleChatSelection(chat.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{chat.title}
|
{chat.title}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-row items-center justify-between w-full">
|
<div className="flex flex-row items-center justify-between w-full">
|
||||||
<div className="flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70">
|
<div className="flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70">
|
||||||
<ClockIcon size={15} />
|
<ClockIcon size={15} />
|
||||||
@@ -277,30 +97,16 @@ const Page = () => {
|
|||||||
{formatTimeDifference(new Date(), chat.createdAt)} Ago
|
{formatTimeDifference(new Date(), chat.createdAt)} Ago
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Delete button - only visible when not in selection mode */}
|
|
||||||
{!selectionMode && (
|
|
||||||
<DeleteChat
|
<DeleteChat
|
||||||
chatId={chat.id}
|
chatId={chat.id}
|
||||||
chats={chats}
|
chats={chats}
|
||||||
setChats={updateChatsAfterDelete}
|
setChats={setChats}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Batch Delete Confirmation Dialog */}
|
|
||||||
<BatchDeleteChats
|
|
||||||
chatIds={selectedChats}
|
|
||||||
chats={chats}
|
|
||||||
setChats={updateChatsAfterDelete}
|
|
||||||
onComplete={handleBatchDeleteComplete}
|
|
||||||
isOpen={isDeleteDialogOpen}
|
|
||||||
setIsOpen={setIsDeleteDialogOpen}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,118 +0,0 @@
|
|||||||
import {
|
|
||||||
Description,
|
|
||||||
Dialog,
|
|
||||||
DialogBackdrop,
|
|
||||||
DialogPanel,
|
|
||||||
DialogTitle,
|
|
||||||
Transition,
|
|
||||||
TransitionChild,
|
|
||||||
} from '@headlessui/react';
|
|
||||||
import { Fragment, useState } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { Chat } from '@/app/library/page';
|
|
||||||
|
|
||||||
interface BatchDeleteChatsProps {
|
|
||||||
chatIds: string[];
|
|
||||||
chats: Chat[];
|
|
||||||
setChats: (chats: Chat[]) => void;
|
|
||||||
onComplete: () => void;
|
|
||||||
isOpen: boolean;
|
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BatchDeleteChats = ({
|
|
||||||
chatIds,
|
|
||||||
chats,
|
|
||||||
setChats,
|
|
||||||
onComplete,
|
|
||||||
isOpen,
|
|
||||||
setIsOpen,
|
|
||||||
}: BatchDeleteChatsProps) => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
if (chatIds.length === 0) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
for (const chatId of chatIds) {
|
|
||||||
await fetch(`/api/chats/${chatId}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newChats = chats.filter(chat => !chatIds.includes(chat.id));
|
|
||||||
setChats(newChats);
|
|
||||||
|
|
||||||
toast.success(`${chatIds.length} thread${chatIds.length > 1 ? 's' : ''} deleted`);
|
|
||||||
onComplete();
|
|
||||||
} catch (err: any) {
|
|
||||||
toast.error('Failed to delete threads');
|
|
||||||
} finally {
|
|
||||||
setIsOpen(false);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
|
||||||
<Dialog
|
|
||||||
as="div"
|
|
||||||
className="relative z-50"
|
|
||||||
onClose={() => {
|
|
||||||
if (!loading) {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogBackdrop className="fixed inset-0 bg-black/30" />
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
|
||||||
<TransitionChild
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-200"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100 scale-200"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
|
|
||||||
<DialogTitle className="text-lg font-medium leading-6 dark:text-white">
|
|
||||||
Delete Confirmation
|
|
||||||
</DialogTitle>
|
|
||||||
<Description className="text-sm dark:text-white/70 text-black/70">
|
|
||||||
Are you sure you want to delete {chatIds.length} selected thread{chatIds.length !== 1 ? 's' : ''}?
|
|
||||||
</Description>
|
|
||||||
<div className="flex flex-row items-end justify-end space-x-4 mt-6">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (!loading) {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="text-black/50 dark:text-white/50 text-sm hover:text-black/70 hover:dark:text-white/70 transition duration-200"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleDelete}
|
|
||||||
className="text-red-400 text-sm hover:text-red-500 transition duration-200"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</DialogPanel>
|
|
||||||
</TransitionChild>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BatchDeleteChats;
|
|
@@ -30,6 +30,18 @@ const openaiChatModels: Record<string, string>[] = [
|
|||||||
displayName: 'GPT-4 omni mini',
|
displayName: 'GPT-4 omni mini',
|
||||||
key: 'gpt-4o-mini',
|
key: 'gpt-4o-mini',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'GPT 4.1 nano',
|
||||||
|
key: 'gpt-4.1-nano',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'GPT 4.1 mini',
|
||||||
|
key: 'gpt-4.1-mini',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'GPT 4.1',
|
||||||
|
key: 'gpt-4.1',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const openaiEmbeddingModels: Record<string, string>[] = [
|
const openaiEmbeddingModels: Record<string, string>[] = [
|
||||||
|
@@ -64,7 +64,7 @@ export const getDocumentsFromLinks = async ({ links }: { links: string[] }) => {
|
|||||||
const splittedText = await splitter.splitText(parsedText);
|
const splittedText = await splitter.splitText(parsedText);
|
||||||
const title = res.data
|
const title = res.data
|
||||||
.toString('utf8')
|
.toString('utf8')
|
||||||
.match(/<title>(.*?)<\/title>/)?.[1];
|
.match(/<title.*>(.*?)<\/title>/)?.[1];
|
||||||
|
|
||||||
const linkDocs = splittedText.map((text) => {
|
const linkDocs = splittedText.map((text) => {
|
||||||
return new Document({
|
return new Document({
|
||||||
|
Reference in New Issue
Block a user