Skip to content

Commit

Permalink
Support viewing and deleting all indexed web pages and local files
Browse files Browse the repository at this point in the history
  • Loading branch information
ahaapple committed Jan 9, 2025
1 parent 79e7fb8 commit cd0ee01
Show file tree
Hide file tree
Showing 16 changed files with 303 additions and 102 deletions.
Original file line number Diff line number Diff line change
@@ -1,58 +1,31 @@
import { FailedUrlTable } from '@/components/failed-url-table';
import { IndexesTable } from '@/components/indexes-table';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Tabs, TabsContent } from '@/components/ui/tabs';
import { UrlTable } from '@/components/url-table';
import { getUserStatistics } from '@/lib/db';
import { getCurrentUser } from '@/lib/session';
import { Link, Search } from 'lucide-react';
import { redirect } from 'next/navigation';

export default async function page() {
const user = await getCurrentUser();
if (!user) {
redirect('/login');
}
const [urls, failedUrls, indexCount, searchCount] = await getUserStatistics(user.id);
const [urls, failedUrls, count] = await getUserStatistics(user.id);

return (
<ScrollArea className="group mx-auto overflow-auto">
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
<div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">Search & Index Statistics</h2>
<h2 className="text-3xl font-bold tracking-tight">Your Indexed Local Files & Web Pages & Bookmarks </h2>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Index Count</CardTitle>
<Link size={18} strokeWidth={2} color="gray"></Link>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-primary">{indexCount}</div>
<p className="pt-2 text-xs text-muted-foreground">bookmarks and urls</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Search Count</CardTitle>
<Search size={18} strokeWidth={2} color="gray"></Search>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-primary">{searchCount}</div>
<p className="pt-2 text-xs text-muted-foreground">AI search</p>
</CardContent>
</Card>
</div>
<div className="grid gap-4 grid-cols-1 ">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Recent Indexed Urls</CardTitle>
<CardDescription>Recent bookmarks and web pages you have indexed</CardDescription>
</CardHeader>
<CardContent className="pl-2">
<UrlTable urls={urls} />
<IndexesTable userId={user.id} initialUrls={urls} totalCount={count} />
</CardContent>
</Card>
</div>
Expand Down
4 changes: 4 additions & 0 deletions frontend/app/[locale]/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { mainNavConfig } from '@/config';
import { DashBoardSidebarNav } from '@/components/dashboard/sidebar-nav';

const sidebarNavItems = [
{
title: 'Indexes',
href: '/indexes',
},
{
title: 'Images',
href: '/images',
Expand Down
15 changes: 11 additions & 4 deletions frontend/app/api/delete-url/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { auth } from '@/auth';
import { removeUrlFromErrorUrls } from '@/lib/db';
import { removeIndexedUrls, removeUrlFromErrorUrls } from '@/lib/db';
import { VECTOR_INDEX_HOST } from '@/lib/env';
import { removeIndex } from '@/lib/index/remove';
import { NextResponse } from 'next/server';

export const runtime = 'edge';
export async function POST(req: Request) {
try {
const { url } = await req.json();
const { url, isSuccess } = await req.json();
const session = await auth();
if (!session?.user) {
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
}

await removeUrlFromErrorUrls(session?.user.id, url);
console.log('deleteUrl', session?.user.id, url);
if (isSuccess) {
await removeIndex(VECTOR_INDEX_HOST, session?.user.id, [url]);
await removeIndexedUrls(session?.user.id, [url]);
} else {
await removeUrlFromErrorUrls(session?.user.id, url);
}
console.log('deleteUrl', session?.user.id, url, isSuccess);

return NextResponse.json({ message: 'success' });
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/failed-url-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function FailedUrlTable(props: { urls: ScoredURL[] }) {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: url.value }),
body: JSON.stringify({ url: url.value, isSuccess: false }),
})
.then((response) => {
if (!response.ok) {
Expand Down
133 changes: 133 additions & 0 deletions frontend/components/indexes-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
'use client';

import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { formatDateTime } from '@/lib/utils';
import { ScoredURL } from '@/lib/types';
import { Loader2, Send, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { getIndexedUrls } from '@/lib/store/indexes';
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from '@/components/ui/pagination';

interface IndexesTableProps {
userId: string;
initialUrls: ScoredURL[];
totalCount: number;
}

export function IndexesTable({ userId, initialUrls, totalCount }: IndexesTableProps) {
const [urls, setUrls] = useState<ScoredURL[]>(initialUrls);
const [currentPage, setCurrentPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const PAGE_SIZE = 20;
const totalPages = Math.ceil(totalCount / PAGE_SIZE);

const fetchPageData = async (page: number) => {
setIsLoading(true);
try {
const offset = (page - 1) * PAGE_SIZE;
const result = await getIndexedUrls(userId, offset, PAGE_SIZE);
setUrls(result);
} catch (error) {
console.error('Error fetching page data:', error);
toast.error('Failed to load data');
} finally {
setIsLoading(false);
}
};

const handlePageChange = async (newPage: number) => {
if (newPage < 1 || newPage > totalPages) return;
setCurrentPage(newPage);
await fetchPageData(newPage);
};

const handleVisit = (url: ScoredURL) => {
window.open(url.value, '_blank');
};

const [deletingId, setDeletingId] = useState<string | null>(null);
const handleDelete = async (url: ScoredURL) => {
try {
if (deletingId) return;
setDeletingId(url.value);
const response = await fetch('/api/delete-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: url.value, isSuccess: true }),
});

if (!response.ok) {
throw new Error('Delete failed');
}

const data = await response.json();
setUrls((prevUrls) => prevUrls.filter((u) => u.value !== url.value));
toast.success('Successfully Deleted');
} catch (error) {
console.error('Error:', error);
toast.error('Failed to delete');
} finally {
setDeletingId(null);
}
};

return (
<div className="space-y-4">
<Table>
<TableHeader>
<TableRow>
<TableHead>Url</TableHead>
<TableHead>Indexed Date</TableHead>
<TableHead>Visit</TableHead>
<TableHead>Delete</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{urls.map((url, index) => (
<TableRow key={index}>
<TableCell>
<div className="font-medium">{url.value}</div>
</TableCell>
<TableCell>{formatDateTime(url.score)}</TableCell>
<TableCell>
<button onClick={() => handleVisit(url)} title="visit" aria-label="Visit link">
<Send size={24} />
</button>
</TableCell>
<TableCell>
<button onClick={() => handleDelete(url)} title="Delete" aria-label="Delete">
{deletingId === url.value ? (
<div className="animate-spin">
<Loader2 size={24} />
</div>
) : (
<Trash2 size={24} />
)}
</button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>

{totalPages > 1 && (
<Pagination>
<PaginationContent>
<PaginationItem className="cursor-pointer hover:bg-accent hover:text-accent-foreground">
<PaginationPrevious onClick={() => handlePageChange(currentPage - 1)} />
</PaginationItem>
<PaginationItem className="flex items-center">
<span>{`Page ${currentPage} of ${totalPages}`}</span>
</PaginationItem>
<PaginationItem className="cursor-pointer hover:bg-accent hover:text-accent-foreground">
<PaginationNext onClick={() => handlePageChange(currentPage + 1)} />
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
);
}
27 changes: 0 additions & 27 deletions frontend/components/url-table.tsx

This file was deleted.

16 changes: 12 additions & 4 deletions frontend/lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,22 @@ export async function incSearchCount(userId: string): Promise<void> {

export async function removeUrlFromErrorUrls(userId: string, url: string) {
const result = await redisDB.zrem(ERROR_URLS_KEY + userId, url);
console.log('removeUrlFromErrorUrls:', result);
return result;
}

export type UserStatistics = [ScoredURL[], ScoredURL[], number | null, string | null];
export async function removeIndexedUrls(userId: string, urls: string | string[]): Promise<number> {
const key = URLS_KEY + userId;
const urlsArray = Array.isArray(urls) ? urls : [urls];
const result = await redisDB.zrem(key, ...urlsArray);
console.log('removeIndexedUrls:', result);
return result;
}

export type UserStatistics = [ScoredURL[], ScoredURL[], number];

export async function getUserStatistics(userId: string): Promise<UserStatistics> {
const [urls, failedUrls, indexCount, searchCount] = await Promise.all([
const [urls, failedUrls, count] = await Promise.all([
redisDB.zrange(URLS_KEY + userId, 0, 19, {
rev: true,
withScores: true,
Expand All @@ -87,7 +96,6 @@ export async function getUserStatistics(userId: string): Promise<UserStatistics>
withScores: true,
}),
redisDB.zcard(URLS_KEY + userId),
redisDB.get(SEARCH_COUNT_KEY + userId),
]);

const scoredURLs: ScoredURL[] = [];
Expand All @@ -106,7 +114,7 @@ export async function getUserStatistics(userId: string): Promise<UserStatistics>
});
}

return [scoredURLs as ScoredURL[], failedUrlss as ScoredURL[], indexCount as number, searchCount as string];
return [scoredURLs as ScoredURL[], failedUrlss as ScoredURL[], count];
}

export async function getUserIndexCount(userId: string): Promise<number> {
Expand Down
32 changes: 32 additions & 0 deletions frontend/lib/index/select-detail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'server-only';

import { API_TOKEN, VECTOR_HOST } from '@/lib/env';
import { log } from '@/lib/log';

export async function selectDetail(userId: string, offset: number = 0, limit: number = 20) {
try {
const response = await fetch(`${VECTOR_HOST}/api/detail/search`, {
method: 'POST',
headers: {
'Authorization': `${API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId: userId, offset: offset, selectFields: ['url', 'create_time'] }),
});
if (!response.ok) {
console.error(`rselectDetail! Status: ${response.status}, StatusText: ${response.statusText}`);
throw new Error(`selectDetail! Status: ${response.status}, StatusText: ${response.statusText}`);
}

return response.json();
} catch (error) {
console.error(`remove url Error! ${error} for user ${userId}`);
log({
service: 'index-url',
action: `error-remove-url`,
error: `${error}`,
userId: userId,
});
throw error;
}
}
21 changes: 21 additions & 0 deletions frontend/lib/store/indexes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use server';

import { redisDB, URLS_KEY } from '@/lib/db';
import { ScoredURL } from '@/lib/types';

export async function getIndexedUrls(userId: string, offset: number, limit: number): Promise<ScoredURL[]> {
const urls = await redisDB.zrange(URLS_KEY + userId, offset, offset + limit - 1, {
rev: true,
withScores: true,
});

const scoredURLs: ScoredURL[] = [];
for (let i = 0; i < urls.length; i += 2) {
scoredURLs.push({
value: urls[i] as string,
score: urls[i + 1] as number,
});
}

return scoredURLs;
}
6 changes: 2 additions & 4 deletions frontend/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,8 @@ export function resolveTime(timestamp: Date | number): string {
}

export function formatDateTime(input: string | number): string {
const date = new Date(input);
const formattedDate = date.toLocaleDateString('en-US');
const formattedTime = date.toLocaleTimeString('en-US', { hour12: false });
return `${formattedDate}, ${formattedTime}`;
const timestamp = typeof input === 'string' ? parseInt(input) : input;
return format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss');
}

export function absoluteUrl(path: string) {
Expand Down
Loading

0 comments on commit cd0ee01

Please sign in to comment.