From 5adbfe14ab0acad38a4f8b14d33d260c7f5f9352 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Wed, 24 Jul 2024 17:43:19 +0530 Subject: [PATCH 1/5] Add a search page that just renders truncated results when you click search --- src/interface/web/app/search/layout.tsx | 24 +++ src/interface/web/app/search/page.tsx | 170 ++++++++++++++++++ .../web/app/search/search.module.css | 12 ++ 3 files changed, 206 insertions(+) create mode 100644 src/interface/web/app/search/layout.tsx create mode 100644 src/interface/web/app/search/page.tsx create mode 100644 src/interface/web/app/search/search.module.css 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..30be4f5d --- /dev/null +++ b/src/interface/web/app/search/page.tsx @@ -0,0 +1,170 @@ +'use client' + +import { Input } from '@/components/ui/input'; + +import { useAuthenticatedData } from '../common/auth'; +import { useEffect, 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 { Folder, FolderOpen, GithubLogo, LinkSimple, MagnifyingGlass, NoteBlank, NotionLogo } from '@phosphor-icons/react'; + +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 ; +} + +function Note(note: SearchResult) { + const isFileNameURL = (note.additional.file || '').startsWith('http'); + const fileName = isFileNameURL ? note.additional.heading : note.additional.file.split('/').pop(); + console.log("note.additional.file", note.additional.file); + console.log("filename", fileName); + return ( + + + +
+ {getNoteTypeIcon(note.additional.source)} +
+
+ + {fileName} + +
+ +
+ {note.entry} +
+
+ + { + isFileNameURL ? + + {note.additional.file} + + : +
+ {note.additional.file} +
+ } +
+
+ ); + +} + +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([]); + const [searchResultsLoading, setSearchResultsLoading] = useState(false); + + useEffect(() => { + setIsMobileWidth(window.innerWidth < 786); + + window.addEventListener('resize', () => { + setIsMobileWidth(window.innerWidth < 786); + }); + + }, []); + + useEffect(() => { + setTitle(isMobileWidth ? '' : 'Search'); + }, [isMobileWidth]); + + function search(query: string) { + 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); + }); + } + + console.log('searchResults', searchResults); + + return ( +
+
+ +
+
+ +
+ { + isMobileWidth &&
Search
+ } +
+ + setSearchQuery(e.target.value)} + type="search" + placeholder="Search Documents" /> + +
+ { + searchResults.length > 0 && +
+ + { + searchResults.map((result, index) => { + return ( + + ); + }) + } + +
+ } +
+
+
+ ); +} 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; + } +} From 75a370cc06ded9215384e2e6a17aa2feb665a7b3 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Wed, 24 Jul 2024 18:00:33 +0530 Subject: [PATCH 2/5] Implement focus mode to click into full text of the note --- src/interface/web/app/search/page.tsx | 65 ++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/src/interface/web/app/search/page.tsx b/src/interface/web/app/search/page.tsx index 30be4f5d..6b85c0d3 100644 --- a/src/interface/web/app/search/page.tsx +++ b/src/interface/web/app/search/page.tsx @@ -9,7 +9,8 @@ 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 { Folder, FolderOpen, GithubLogo, LinkSimple, MagnifyingGlass, NoteBlank, NotionLogo } from '@phosphor-icons/react'; +import { ArrowLeft, ArrowRight, Folder, FolderOpen, GithubLogo, LinkSimple, MagnifyingGlass, NoteBlank, NotionLogo } from '@phosphor-icons/react'; +import { Button } from '@/components/ui/button'; interface AdditionalData { file: string; @@ -36,16 +37,21 @@ function getNoteTypeIcon(source: string) { return ; } -function Note(note: SearchResult) { +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(); - console.log("note.additional.file", note.additional.file); - console.log("filename", fileName); + return ( -
+
{getNoteTypeIcon(note.additional.source)}
@@ -57,6 +63,7 @@ function Note(note: SearchResult) {
{note.entry}
+ { @@ -72,7 +79,38 @@ function Note(note: SearchResult) { ); +} +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() { @@ -82,6 +120,7 @@ export default function Search() { const [title, setTitle] = useState('Search'); const [searchResults, setSearchResults] = useState([]); const [searchResultsLoading, setSearchResultsLoading] = useState(false); + const [focusSearchResult, setFocusSearchResult] = useState(null); useEffect(() => { setIsMobileWidth(window.innerWidth < 786); @@ -150,13 +189,25 @@ export default function Search() {
{ - searchResults.length > 0 && + focusSearchResult && +
+ + {focusNote(focusSearchResult)} +
+ } + { + !focusSearchResult && searchResults.length > 0 &&
{ searchResults.map((result, index) => { return ( - + ); }) } From 19cd607c96ceb3907fe4f538f03677804a3b2bf6 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Wed, 24 Jul 2024 18:28:23 +0530 Subject: [PATCH 3/5] Style the see content button correctly --- src/interface/web/app/search/page.tsx | 37 +++++++++++++-------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/interface/web/app/search/page.tsx b/src/interface/web/app/search/page.tsx index 6b85c0d3..e221fa08 100644 --- a/src/interface/web/app/search/page.tsx +++ b/src/interface/web/app/search/page.tsx @@ -50,20 +50,20 @@ function Note(props: NoteResultProps) { return ( - -
- {getNoteTypeIcon(note.additional.source)} -
+ + {getNoteTypeIcon(note.additional.source)} {fileName}
-
+
{note.entry}
- + { @@ -90,20 +90,19 @@ function focusNote(note: SearchResult) { {fileName} - - { - isFileNameURL ? - - {note.additional.file} - - : -
- {note.additional.file} -
- } -
- + + { + isFileNameURL ? + + {note.additional.file} + + : +
+ {note.additional.file} +
+ } +
{note.entry} From 662dffea3b25ab4b2cf679e5883c5259f36d2674 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Wed, 24 Jul 2024 19:28:38 +0530 Subject: [PATCH 4/5] Press enter to search --- src/interface/web/app/search/page.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/interface/web/app/search/page.tsx b/src/interface/web/app/search/page.tsx index e221fa08..2ffdd8b3 100644 --- a/src/interface/web/app/search/page.tsx +++ b/src/interface/web/app/search/page.tsx @@ -172,7 +172,7 @@ export default function Search() {
-
+
{ isMobileWidth &&
Search
} @@ -180,11 +180,12 @@ export default function Search() { setSearchQuery(e.target.value)} + onChange={(e) => setSearchQuery(e.currentTarget.value)} + onKeyDown={(e) => e.key === 'Enter' && search(searchQuery)} type="search" placeholder="Search Documents" />
{ From 7482797605f383ce13d7d07f3b35859979b116c9 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Thu, 25 Jul 2024 13:00:28 +0530 Subject: [PATCH 5/5] Add some better default states for no files found, prompt to search. Add link to search in the file search compnoent in side panel --- .../sidePanel/chatHistorySidePanel.tsx | 2 +- src/interface/web/app/search/page.tsx | 104 ++++++++++++++++-- 2 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx b/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx index 122deba7..3913e068 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/page.tsx b/src/interface/web/app/search/page.tsx index 2ffdd8b3..d9ccc679 100644 --- a/src/interface/web/app/search/page.tsx +++ b/src/interface/web/app/search/page.tsx @@ -3,14 +3,15 @@ import { Input } from '@/components/ui/input'; import { useAuthenticatedData } from '../common/auth'; -import { useEffect, useState } from 'react'; +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, Folder, FolderOpen, GithubLogo, LinkSimple, MagnifyingGlass, NoteBlank, NotionLogo } from '@phosphor-icons/react'; +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; @@ -37,6 +38,25 @@ function getNoteTypeIcon(source: string) { 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; @@ -117,9 +137,12 @@ export default function Search() { const [searchQuery, setSearchQuery] = useState(''); const [isMobileWidth, setIsMobileWidth] = useState(false); const [title, setTitle] = useState('Search'); - const [searchResults, setSearchResults] = useState([]); + 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); @@ -128,13 +151,15 @@ export default function Search() { setIsMobileWidth(window.innerWidth < 786); }); + setExampleQuery(naturalLanguageSearchQueryExamples[Math.floor(Math.random() * naturalLanguageSearchQueryExamples.length)]); + }, []); useEffect(() => { setTitle(isMobileWidth ? '' : 'Search'); }, [isMobileWidth]); - function search(query: string) { + function search() { if (searchResultsLoading) { return; } @@ -158,6 +183,31 @@ export default function Search() { }); } + 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 ( @@ -179,12 +229,13 @@ export default function Search() {
setSearchQuery(e.currentTarget.value)} - onKeyDown={(e) => e.key === 'Enter' && search(searchQuery)} + onKeyDown={(e) => e.key === 'Enter' && search()} type="search" placeholder="Search Documents" /> -
@@ -199,7 +250,7 @@ export default function Search() {
} { - !focusSearchResult && searchResults.length > 0 && + !focusSearchResult && searchResults && searchResults.length > 0 &&
{ @@ -214,6 +265,45 @@ export default function Search() {
} + { + 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 +
+ +
+
+ }