diff --git a/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx b/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx index 66fb6925..705bcbc1 100644 --- a/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx +++ b/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx @@ -203,7 +203,7 @@ function FilesMenu(props: FilesMenuProps) { - No results found. + No results found. Try advanced search. { diff --git a/src/interface/web/app/search/layout.tsx b/src/interface/web/app/search/layout.tsx new file mode 100644 index 00000000..ed06f884 --- /dev/null +++ b/src/interface/web/app/search/layout.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from "next"; + +import "../globals.css"; + + +export const metadata: Metadata = { + title: "Khoj AI - Search", + description: "Search through all the documents you've shared with Khoj AI using natural language queries.", + icons: { + icon: '/static/favicon.ico', + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+ {children} +
+ ); +} diff --git a/src/interface/web/app/search/page.tsx b/src/interface/web/app/search/page.tsx new file mode 100644 index 00000000..d9ccc679 --- /dev/null +++ b/src/interface/web/app/search/page.tsx @@ -0,0 +1,311 @@ +'use client' + +import { Input } from '@/components/ui/input'; + +import { useAuthenticatedData } from '../common/auth'; +import { useEffect, useRef, useState } from 'react'; +import SidePanel from '../components/sidePanel/chatHistorySidePanel'; +import NavMenu from '../components/navMenu/navMenu'; +import styles from './search.module.css'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { ArrowLeft, ArrowRight, FileDashed, FileMagnifyingGlass, Folder, FolderOpen, GithubLogo, Lightbulb, LinkSimple, MagnifyingGlass, NoteBlank, NotionLogo } from '@phosphor-icons/react'; +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; + +interface AdditionalData { + file: string; + source: string; + compiled: string; + heading: string; +} + +interface SearchResult { + type: string; + additional: AdditionalData; + entry: string; + score: number; + "corpus-id": string; +} + +function getNoteTypeIcon(source: string) { + if (source === 'notion') { + return ; + } + if (source === 'github') { + return ; + } + return ; +} + +const naturalLanguageSearchQueryExamples = [ + "What does the paper say about climate change?", + "Making a cappuccino at home", + "Benefits of eating mangoes", + "How to plan a wedding on a budget", + "Appointment with Dr. Makinde on 12th August", + "Class notes lecture 3 on quantum mechanics", + "Painting concepts for acrylics", + "Abstract from the paper attention is all you need", + "Climbing Everest without oxygen", + "Solving a rubik's cube in 30 seconds", + "Facts about the planet Mars", + "How to make a website using React", + "Fish at the bottom of the ocean", + "Fish farming Kenya 2021", + "How to make a cake without an oven", + "Installing a solar panel at home", +] + +interface NoteResultProps { + note: SearchResult; + setFocusSearchResult: (note: SearchResult) => void; +} + +function Note(props: NoteResultProps) { + const note = props.note; + const isFileNameURL = (note.additional.file || '').startsWith('http'); + const fileName = isFileNameURL ? note.additional.heading : note.additional.file.split('/').pop(); + + return ( + + + + {getNoteTypeIcon(note.additional.source)} + + + {fileName} + + + +
+ {note.entry} +
+ +
+ + { + isFileNameURL ? + + {note.additional.file} + + : +
+ {note.additional.file} +
+ } +
+
+ ); +} + +function focusNote(note: SearchResult) { + const isFileNameURL = (note.additional.file || '').startsWith('http'); + const fileName = isFileNameURL ? note.additional.heading : note.additional.file.split('/').pop(); + return ( + + + + {fileName} + + + + { + isFileNameURL ? + + {note.additional.file} + + : +
+ {note.additional.file} +
+ } +
+ +
+ {note.entry} +
+
+
+ ); +} + +export default function Search() { + const authenticatedData = useAuthenticatedData(); + const [searchQuery, setSearchQuery] = useState(''); + const [isMobileWidth, setIsMobileWidth] = useState(false); + const [title, setTitle] = useState('Search'); + const [searchResults, setSearchResults] = useState(null); + const [searchResultsLoading, setSearchResultsLoading] = useState(false); + const [focusSearchResult, setFocusSearchResult] = useState(null); + const [exampleQuery, setExampleQuery] = useState(''); + const searchTimeoutRef = useRef(null); + + + useEffect(() => { + setIsMobileWidth(window.innerWidth < 786); + + window.addEventListener('resize', () => { + setIsMobileWidth(window.innerWidth < 786); + }); + + setExampleQuery(naturalLanguageSearchQueryExamples[Math.floor(Math.random() * naturalLanguageSearchQueryExamples.length)]); + + }, []); + + useEffect(() => { + setTitle(isMobileWidth ? '' : 'Search'); + }, [isMobileWidth]); + + function search() { + if (searchResultsLoading) { + return; + } + + if (!searchQuery.trim()) { + return; + } + + const apiUrl = `/api/search?q=${encodeURIComponent(searchQuery)}&client=web`; + fetch(apiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + } + }).then(response => response.json()) + .then(data => { + setSearchResults(data); + setSearchResultsLoading(false); + }).catch((error) => { + console.error('Error:', error); + }); + } + + useEffect(() => { + if (!searchQuery.trim()) { + return; + } + + setFocusSearchResult(null); + + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + + if (searchQuery.trim()) { + searchTimeoutRef.current = setTimeout(() => { + search(); + }, 750); // 1000 milliseconds = 1 second + } + + return () => { + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + }; + + }, [searchQuery]); + + console.log('searchResults', searchResults); + + return ( +
+
+ +
+
+ +
+ { + isMobileWidth &&
Search
+ } +
+ + setSearchQuery(e.currentTarget.value)} + onKeyDown={(e) => e.key === 'Enter' && search()} + type="search" + placeholder="Search Documents" /> + +
+ { + focusSearchResult && +
+ + {focusNote(focusSearchResult)} +
+ } + { + !focusSearchResult && searchResults && searchResults.length > 0 && +
+ + { + searchResults.map((result, index) => { + return ( + + ); + }) + } + +
+ } + { + searchResults == null && + + + + + + + Search across your documents + + + + {exampleQuery} + + + } + { + searchResults && searchResults.length === 0 && + + + + + + + No documents found + + + +
+ To use search, upload your docs to your account. +
+ +
+ Learn More +
+ +
+
+ } +
+
+
+ ); +} diff --git a/src/interface/web/app/search/search.module.css b/src/interface/web/app/search/search.module.css new file mode 100644 index 00000000..b063fde6 --- /dev/null +++ b/src/interface/web/app/search/search.module.css @@ -0,0 +1,12 @@ +div.searchLayout { + display: grid; + grid-template-columns: auto 1fr; + gap: 1rem; + height: 100vh; +} + +@media screen and (max-width: 768px) { + div.searchLayout { + gap: 0; + } +}