Skip to content

Commit

Permalink
AI-Powered Search History version 1
Browse files Browse the repository at this point in the history
  • Loading branch information
ahaapple committed Jan 6, 2025
1 parent d962895 commit 7eb615a
Show file tree
Hide file tree
Showing 12 changed files with 506 additions and 129 deletions.
34 changes: 34 additions & 0 deletions frontend/app/api/history-search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { auth } from '@/auth';
import { API_TOKEN, VECTOR_HOST } from '@/lib/env';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get('q');

const session = await auth();
if (!session?.user) {
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
}
const searchUrl = `${VECTOR_HOST}/api/vector/search`;
const response = await fetch(searchUrl, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: API_TOKEN!,
},
body: JSON.stringify({
userId: session?.user.id,
query,
}),
});

if (!response.ok) {
throw new Error(`Error! status: ${response.status}`);
}

const result = await response.json();
console.log(result);

return NextResponse.json(result);
}
116 changes: 116 additions & 0 deletions frontend/components/modal/search-model.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// components/SearchDialog.tsx
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Loader2, MessageCircle } from 'lucide-react';
import { ScrollArea } from '@/components/ui/scroll-area';

interface SearchResult {
id: string;
title: string;
url: string;
}

interface SearchDialogProps {
openSearch: boolean;
onOpenModelChange: (open: boolean) => void;
}

interface SearchResult {
id: string;
title: string;
url: string;
text: string;
}

export function SearchDialog({ openSearch: open, onOpenModelChange: onOpenChange }: SearchDialogProps) {
const router = useRouter();
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const [isLoading, setIsLoading] = useState(false);

const handleSearch = async (searchQuery: string) => {
if (!searchQuery.trim()) {
setResults([]);
return;
}

setIsLoading(true);
try {
const response = await fetch(`/api/history-search?q=${encodeURIComponent(searchQuery)}`);
const data = await response.json();
console.log(data);
setResults(data);
} catch (error) {
console.error('search error:', error);
setResults([]);
} finally {
setIsLoading(false);
}
};

const handleResultClick = (url: string) => {
router.push('/search/' + url);
onOpenChange(false);
};

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>AI-Powered Search History</DialogTitle>
</DialogHeader>

<div className="space-y-4">
<div className="flex gap-2">
<Input
type="text"
placeholder="Search your search history"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="flex-1"
autoFocus
/>
<Button onClick={() => handleSearch(query)} disabled={isLoading}>
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Search'}
</Button>
</div>

<ScrollArea className="h-[400px] rounded-md border">
{isLoading ? (
<div className="flex items-center justify-center h-full">
<div className="text-sm text-muted-foreground">Searching ...</div>
</div>
) : results.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="text-sm text-muted-foreground">No Result</div>
</div>
) : (
<div className="divide-y">
{results.map((result) => (
<div
key={result.id}
className="flex items-start gap-3 p-4 hover:bg-muted/50 cursor-pointer transition-colors"
onClick={() => handleResultClick(result.url)}
>
<div className="mt-1">
<MessageCircle className="h-5 w-5 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-medium leading-none mb-1 truncate">{result.title}</h4>
<p className="text-sm text-muted-foreground line-clamp-2">{result.text}</p>
</div>
</div>
))}
</div>
)}
</ScrollArea>
</div>
</DialogContent>
</Dialog>
);
}
21 changes: 7 additions & 14 deletions frontend/components/sidebar/search-history.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
import * as React from 'react';

import Link from 'next/link';
import Image from 'next/image';
import dynamic from 'next/dynamic';

import { SidebarList } from './sidebar-list';
import { siteConfig } from '@/config';
import { SidebarClose } from '@/components/sidebar/sidebar-close';
import { SignInButton } from '@/components/layout/sign-in-button';
import { User } from '@/lib/types';
import { NewSearchButton } from '@/components/shared/new-search-button';
import { buttonVariants } from '@/components/ui/button';

const SidebarHeader = dynamic(() => import('@/components/sidebar/sidebar-header').then((mod) => mod.SidebarHeader), {
ssr: false,
loading: () => <div className="flex items-center mt-4 md:col-span-1 mx-4 h-[52px]" />,
});

interface SearchHistoryProps {
user: User;
}

export async function SearchHistory({ user }: SearchHistoryProps) {
return (
<div className="flex flex-col h-full">
<div className="flex items-center mt-4 md:col-span-1 mx-4">
<Link href="/" prefetch={false} className="items-center space-x-2 flex">
<Image src={'/logo.png'} width="24" height="24" alt="MemFree Logo"></Image>
<span className=" mx-2 font-urban text-xl font-bold">{siteConfig.name}</span>
</Link>
<div className="ml-auto">
<SidebarClose />
</div>
</div>
<SidebarHeader />
{!user && <SignInButton />}

<div className="flex flex-col my-2 px-4 space-y-2">
Expand Down
3 changes: 1 addition & 2 deletions frontend/components/sidebar/sidebar-close.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ export function SidebarClose() {
toggleSidebar();
}}
>
<ArrowLeftToLine className="size-3 text-inherit" strokeWidth={1.5} />
<span className="sr-only">Toggle Sidebar</span>
<ArrowLeftToLine className="size-4" />
</Button>
)}
</>
Expand Down
35 changes: 35 additions & 0 deletions frontend/components/sidebar/sidebar-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client';

import * as React from 'react';

import Link from 'next/link';
import Image from 'next/image';

import { siteConfig } from '@/config';
import { SidebarClose } from '@/components/sidebar/sidebar-close';
import { Button } from '@/components/ui/button';
import { SearchDialog } from '@/components/modal/search-model';
import { Search } from 'lucide-react';

export async function SidebarHeader() {
const [open, setOpen] = React.useState(false);
return (
<div className="flex items-center mt-4 md:col-span-1 mx-4">
<Link href="/" prefetch={false} className="items-center space-x-2 flex">
<Image src={'/logo.png'} width="24" height="24" alt="MemFree Logo"></Image>
<span className=" mx-2 font-urban text-xl font-bold">{siteConfig.name}</span>
</Link>
<div className="flex ml-auto space-x-2">
<Button
variant="ghost"
className="hidden border-solid shadow-sm border-gray-200 dark:text-white dark:hover:bg-gray-700 rounded-full size-9 p-0 lg:flex"
onClick={() => setOpen(true)}
>
<Search className="size-4" />
</Button>
<SidebarClose />
</div>
<SearchDialog openSearch={open} onOpenModelChange={setOpen} />
</div>
);
}
106 changes: 75 additions & 31 deletions vector/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,96 @@ export class LanceDB {
private config: DatabaseConfig;
private db: any;
private dbSchema: DBSchema;
private tableCreationLocks = new Map<string, Promise<lancedb.Table>>();

constructor(config: DatabaseConfig, schema: DBSchema) {
this.config = config;
this.dbSchema = schema;
}

private async withLock<T>(
key: string,
fn: () => Promise<lancedb.Table>
): Promise<lancedb.Table> {
if (this.tableCreationLocks.has(key)) {
return this.tableCreationLocks.get(key)! as Promise<lancedb.Table>;
}

const promise = fn().finally(() => {
this.tableCreationLocks.delete(key);
});
this.tableCreationLocks.set(key, promise);
return promise;
}

async getTable(tableName: string): Promise<lancedb.Table> {
try {
if (!this.db) {
await this.connect();
}

if (!this.db) {
throw new Error("Database connection not established.");
}

if ((await this.db.tableNames()).includes(tableName)) {
return this.db.openTable(tableName);
} else {
// to avoid Conflicting Append and Overwrite Transactions
return this.withLock(tableName, async () => {
// double check if table is created by another thread
const currentTableNames = await this.db.tableNames();
if (currentTableNames.includes(tableName)) {
return this.db.openTable(tableName);
}

console.log("Creating table", tableName);
return this.db.createEmptyTable(tableName, this.dbSchema.schema, {
mode: "create",
existOk: false,
});
});
}
} catch (error) {
console.error("Error getting table", tableName, error);
throw error;
}
}

private isS3Config(options: any): options is S3Config {
return "bucket" in options;
}

async connect(): Promise<any> {
if (this.config.type === "s3") {
if (!this.isS3Config(this.config.options)) {
throw new Error("Invalid S3 configuration");
}

const { bucket, awsAccessKeyId, awsSecretAccessKey, region, s3Express } =
this.config.options || {};
this.db = await lancedb.connect(bucket, {
storageOptions: {
try {
if (this.config.type === "s3") {
if (!this.isS3Config(this.config.options)) {
throw new Error("Invalid S3 configuration");
}

const {
bucket,
awsAccessKeyId,
awsSecretAccessKey,
region,
s3Express,
},
});
} else {
const { localDirectory } =
(this.config.options as LocalConfig) || process.cwd();
this.db = await lancedb.connect(localDirectory);
}
return this.db;
}

async getTable(tableName: string): Promise<lancedb.Table> {
if (!this.db) {
await this.connect();
}

if ((await this.db.tableNames()).includes(tableName)) {
return this.db.openTable(tableName);
} else {
return this.db.createEmptyTable(tableName, this.dbSchema.schema, {
mode: "create",
existOk: true,
});
} = this.config.options || {};
this.db = await lancedb.connect(bucket, {
storageOptions: {
awsAccessKeyId,
awsSecretAccessKey,
region,
s3Express,
},
});
} else {
const { localDirectory } =
(this.config.options as LocalConfig) || process.cwd();
this.db = await lancedb.connect(localDirectory);
}
} catch (error) {
console.error("Error connecting to database", error);
throw error;
}
}

Expand Down
Loading

0 comments on commit 7eb615a

Please sign in to comment.